Back to Blog
Java

Java Package-Private: Default Access in Practice

Learn how Java's package-private (default) access works, when to use it, and how it differs from public, protected, and private.

Javaaccess modifiersencapsulationpackage-privatedefault access
Diagram showing Java package-private access boundaries between classes in the same package

Package-private is Java's default access level: a class, method, or field declared without public, protected, or private is visible only to other classes in the same package. It is not a keyword; it is the absence of an explicit modifier. This article explains how package-private behaves, how it compares with the other access levels, and when to use it.

What Does Package-Private Mean in Java?

The term package-private refers to the default access level in Java. When you declare a class, method, or field without public, protected, or private, it becomes accessible only to classes in the same package. For example:

class PackagePrivateClass { int count; // package-private field void increment() { // package-private method count++; } }

Here, PackagePrivateClass and its members are accessible only from other classes in the same package. A class in a different package cannot reference them, even if that class is a subclass. This behavior is defined by the Java Language Specification and is consistent across Java versions.

In the access hierarchy, package-private is narrower than protected and broader than private. It is more restrictive than protected because subclasses outside the package cannot access package-private members, but less restrictive than private, which restricts access to the enclosing class only.

How Package-Private Compares to Other Access Levels

Understanding the full access hierarchy helps you decide when to use package-private. The following table maps each modifier to its visibility scope:

ModifierSame classSame packageSubclass (different package)Any class
privateYesNoNoNo
package-privateYesYesNoNo
protectedYesYesYesNo
publicYesYesYesYes

Package-private is the only level that does not have a keyword. It is the default when no modifier is written. This often surprises developers coming from languages like C# where internal is explicit. In Java, forgetting to add public to a class you intended to expose will silently make it package-private, which can cause compilation errors in other packages.

Declaring Package-Private Members

You can apply package-private to top-level classes, nested classes, methods, and fields. The syntax is simply to omit the modifier. Here is a more complete example:

package com.example.util; class StringHelper { static String trimAndLower(String input) { return input.trim().toLowerCase(); } } public class TextProcessor { StringHelper helper = new StringHelper(); // package-private field public String process(String raw) { return StringHelper.trimAndLower(raw); } }

In this code, StringHelper is package-private, so it is only visible inside com.example.util. The helper field in TextProcessor is also package-private. A class in another package cannot create a StringHelper instance or access the helper field directly. This allows you to hide implementation details that are only relevant within a package.

One important detail: a package-private top-level class cannot be referenced from outside its package, even if it implements a public interface or extends a public class. The reference itself is inaccessible. This is a common source of confusion when refactoring code.

When Package-Private Is the Right Choice

Package-private is useful when you have a group of classes that cooperate closely within the same package but should not expose their internals to the rest of the application. Common scenarios include:

  • Internal helper classes: A package may contain a public facade class that delegates to several package-private support classes. Those helpers do not need to be part of the public API.
  • Package-level cohesion: When multiple classes in a package share a common data structure or utility, making those members package-private keeps the coupling explicit and contained.
  • Testing hooks: Unit tests placed in the same package can access package-private members, which allows you to test internal behavior without making it public.

For example, consider a package that implements a custom collection. The main public class might be OrderedList, but the internal node class and the comparison logic can be package-private. This keeps the public surface small and reduces the risk of misuse by external code.

Package-Private and Testing

A practical benefit of package-private is that test classes in the same package can access the members directly. This is often used to test internal logic without exposing it through public APIs. Suppose you have a package-private method that performs a complex calculation. A test class in the same package can call it directly:

package com.example.calc; public class CalculatorTest { @Test public void testInternalCalculation() { Calculator calc = new Calculator(); int result = calc.internalAdd(2, 3); // package-private method assertEquals(5, result); } }

If internalAdd were private, this test would not compile. By keeping it package-private, you allow white-box testing while still hiding the method from external consumers. This is a common pattern in library development, where the public API remains minimal but internal correctness is verified.

However, this also means that any class in the same package can call the method, not just tests. If your package contains untrusted or unrelated classes, package-private does not provide strong isolation. It is a design choice, not a security boundary.

Package-Private in Java Modules

Since Java 9, the module system adds another layer of access control. A package that is not exported from a module is inaccessible to code outside the module, regardless of whether its classes are public or package-private. Inside the module, package-private members are visible only to classes in the same package, as usual. An unexported package can therefore hide even public types from external consumers; package-private still enforces boundaries within the package.

When migrating an existing application to modules, you may need to decide which packages to export. Package-private members do not affect module exports, but they do affect how classes within the module interact. This is worth reviewing during modularization to avoid accidentally exposing internals through exported packages.

Common Pitfalls and Maintenance Concerns

The main risk with package-private is accidental omission. If you forget to write public on a class that should be reachable from other packages, you get a compile-time error. The fix is straightforward, but the error message can be confusing if the class is referenced from a different package. The compiler reports that the class is not visible, which often leads developers to search for a missing import rather than a missing modifier.

Another pitfall is expecting package-private members to be accessible from another package. If you later move a class to a different package, existing references will no longer compile unless you update them or widen the access level. This coupling is intentional but can be fragile. When you design a package, think about which members truly need to be shared within the package and which should be private to the class. Overusing package-private can create hidden dependencies that are hard to trace.

From a maintainability perspective, package-private is most effective when the package is cohesive and stable. If your package changes frequently, the internal access boundaries will need to be adjusted often. In contrast, private gives you the freedom to refactor a class without affecting the rest of the package. Use package-private only when you explicitly want to share state or behavior among a fixed set of classes.

One more consideration: package-private constructors can be used to enforce factory patterns within a package. If a class's constructor is package-private, only classes in the same package can instantiate it directly. This is useful for controlling object creation without exposing the constructor publicly, which is a common pattern in dependency injection frameworks and builder implementations.

Java Package-Private: Practical Usage and Code Examples | RYUSLOG DEV