Java Package Private Access: How It Works and When to Use It
java package private access: Understand Java package-private access: how the default modifier works, its behavior across packages and subclasses, and when to use it fo...
java package private access requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
In Java, the absence of an access modifier on a class, method, or field means it is package-private. This is the default access level, and it is often overlooked because it is invisible in the source code. Package-private members are accessible only from classes in the same package, which makes them a useful tool for structuring code that should remain internal to a package without being fully private to a single class.
The Default Access Level in Java
When you write a class member without public, protected, or private, Java applies package-private access. For example:
package com.example.storage; class Cache { int maxEntries; void evict() { // implementation } }
Here, Cache, maxEntries, and evict() are all package-private. They are accessible from any other class in com.example.storage, but not from classes in other packages. This is a deliberate design choice: the member is part of the package's internal contract, not its public API.
The same rule applies to top-level classes. A top-level class with no modifier is package-private, meaning it can only be referenced from within its own package. This is common for helper classes that support the public classes in a package but should not be exposed to external consumers.
How Package-Private Members Behave Across Packages
Package-private access is strictly limited to the package. A class in another package cannot access a package-private member, even if that class is a subclass. This is a key difference from protected, which allows access from subclasses in other packages.
Consider this example:
package com.example.storage; public class Storage { void flush() { // package-private } }
A subclass in another package:
package com.example.client; import com.example.storage.Storage; public class RemoteStorage extends Storage { void test() { flush(); // compile error: flush() is not accessible } }
The compiler rejects the call because flush() is not visible outside com.example.storage. This behavior is consistent regardless of inheritance. Package-private access is about the package boundary, not the class hierarchy.
Using Package-Private for Internal Implementation Details
Package-private members are ideal for code that multiple classes in the same package need to share, but that should not be part of the public API. This keeps the public surface small and reduces the risk of external code depending on implementation details.
A common pattern is a package-private utility class:
package com.example.validation; class StringValidator { static boolean isValid(String input) { return input != null && !input.isBlank(); } }
The public classes in the package can call StringValidator.isValid() without exposing the validator itself. This improves maintainability because you can change the internal implementation without breaking external callers.
Package-private fields are more controversial. They allow direct access from other classes in the same package, which can break encapsulation if used carelessly. However, they are sometimes appropriate for tightly coupled classes that are part of the same logical component, such as a package that implements a data structure with internal nodes.
Package-Private vs. Protected: What Changes for Subclasses
The choice between package-private and protected often confuses developers. Both allow access from classes in the same package. The difference is that protected also allows access from subclasses in other packages, while package-private does not.
For example:
package com.example.base; public class Base { void packagePrivateMethod() { } protected void protectedMethod() { } }
A subclass in another package can call protectedMethod() but not packagePrivateMethod().
This distinction matters when you are designing a class hierarchy. If you intend subclasses outside the package to use a member, you must mark it protected. If the member is only for internal package collaboration, package-private is the better choice because it prevents external subclasses from depending on it.
Package-Private and Java Modules (JPMS)
Java modules, introduced in Java 9, add another layer of access control. A module can export packages to make their public types available to other modules. However, package-private members remain inaccessible outside their package even if the package is exported. The module system does not change the behavior of package-private access.
What modules do affect is whether a package is visible at all. If a package is not exported, even its public classes are inaccessible outside the module. But within the same module, package-private members behave exactly as they do without modules. This means package-private access remains a useful way to hide implementation details even inside a well-encapsulated module.
One subtlety is that the module system allows opens directives for reflection, but that does not grant compile-time access to package-private members. Reflection can bypass access checks, but that is a separate concern and not part of the language's access control model.
Common Mistakes and Subtle Edge Cases
A frequent mistake is assuming that package-private members are accessible from subclasses in other packages. As shown earlier, this is false. Another mistake is forgetting that a top-level class without a modifier is package-private, which can cause a ClassNotFoundException or IllegalAccessError when trying to load it from another package at runtime.
Edge cases also arise with nested classes. A nested class is a member of its enclosing class, so its access modifier is interpreted in the context of the enclosing class. A package-private nested class is accessible only from the same package, but its enclosing class's access level also matters. If the enclosing class is public, the nested class is still package-private.
Another subtlety is that package-private access is not inherited. If a subclass in the same package overrides a package-private method, that override is also package-private. If the subclass is in a different package, it cannot override the method because it cannot access it. This can lead to confusing behavior when you expect inheritance to work across packages.
Design Tradeoffs and Maintainability
Choosing package-private access is a tradeoff between encapsulation and convenience. It provides more encapsulation than public or protected because it limits access to a small, well-defined group of classes. It is less restrictive than private, which can be too limiting when several classes need to cooperate closely.
The main risk is that package-private members become a de facto API for the package. Other developers working in the same package may start relying on them, making future changes harder. To mitigate this, keep package-private members minimal and document that they are internal.
When deciding whether to use package-private, ask whether the member is needed by more than one class in the package and whether it should be hidden from external code. If the answer to both is yes, package-private is likely the right choice. If only one class uses it, prefer private. If subclasses in other packages need it, use protected.
In practice, package-private access is most valuable in cohesive packages that implement a single feature. It allows you to structure the implementation across several classes without exposing that structure to the outside world. It is a quiet but essential tool for building maintainable Java applications.