Access Levels Across Packages
Know exactly what private, package-private, protected and public allow across packages, and use package-private to keep a feature's internals hidden.
private int a; // this class only
int b; // package-private: this package
protected int c; // this package + subclasses elsewhere
public int d; // everyone (if the package is exported)Java has four access levels, and three of them are about packages. Writing no modifier at all does not mean private: it means package-private, visible to every class in the same package and to nothing else.
| Modifier | Same class | Same package | Subclass in another package | Any other package |
|---|---|---|---|---|
private | yes | no | no | no |
| (none) | yes | yes | no | no |
protected | yes | yes | yes, through its own type | no |
public | yes | yes | yes | yes |
protected is wider than you think
protected includes package access, so every class in the same package can use a protected member, subclass or not. Across packages, a subclass can use an inherited protected member only through this or a reference of its own type. Calling it on a reference typed as the parent class fails with "has protected access". Most service code never needs protected; it belongs to classes designed to be extended, such as framework base classes.
Top-level types and defaults
A top-level class, record, enum or interface is either public or package-private. A file holds at most one public top-level type and must be named after it. Some members have a different default: interface methods and constants are implicitly public, enum constructors are implicitly private, and the accessor methods a record generates are public.
Package-private as a design tool
The most useful level in a service is the one with no keyword. Put a feature in one package, make the type other features call public, and leave repositories, mappers and helper classes package-private. The compiler then enforces the boundary: a class in com.example.shop.customers that tries to use JdbcOrderRepository fails with "is not public in com.example.shop.orders; cannot be accessed from outside package". Nobody has to remember a convention.
Tests fit this design. A test class in src/test/java under the same package name shares the package at run time, so it can construct package-private classes directly. JUnit 5 test classes and methods can themselves be package-private, and that is the usual style. Spring can create package-private @Component classes and call package-private constructors, so keeping a bean's implementation hidden costs nothing.
Common mistakes
- Making everything public because the IDE template did. Every public type is a promise other packages may start relying on.
- Splitting one feature across
serviceandrepositorypackages, which forces the repository to be public. - Expecting
com.example.shop.orders.webto see package-private members ofcom.example.shop.orders. It cannot; they are different packages.
Modules add one more layer on top of this table: in a named module, a public type is only visible to other modules if its package is exported.
Example
// src/main/java/com/example/shop/payments/PaymentGateway.java
package com.example.shop.payments;
// Public: other features depend on this interface.
public interface PaymentGateway {
Receipt charge(String customerId, long amountPence);
}
// src/main/java/com/example/shop/payments/Receipt.java
package com.example.shop.payments;
public record Receipt(String reference, long amountPence) {}
// src/main/java/com/example/shop/payments/CardGateway.java
package com.example.shop.payments;
// Package-private: callers cannot name this class, let alone construct it.
final class CardGateway implements PaymentGateway {
private final String apiKey; // private: nobody outside this class needs it
CardGateway(String apiKey) {
this.apiKey = apiKey;
}
public Receipt charge(String customerId, long amountPence) {
return new Receipt("pay_" + customerId, amountPence);
}
}
// src/main/java/com/example/shop/payments/Payments.java
package com.example.shop.payments;
// The single public way in.
public final class Payments {
private Payments() {}
public static PaymentGateway gateway(String apiKey) {
return new CardGateway(apiKey);
}
}When to use it
- Keep a JDBC repository package-private so other features must go through the public service.
- Unit test a package-private mapper from a test class in the same package without widening its access.
- Expose a payment client as a public interface plus a factory while the implementation and its API key stay hidden.
More examples
Reading access levels with reflection
Package-private has no keyword, so reflection reports no modifier at all. Frameworks that check visibility, such as a JSON library deciding which fields to read, see exactly this.
import java.lang.reflect.Modifier;
import java.util.List;
public class Main {
static class Account {
private long balancePence;
long version;
protected String owner;
public String id;
}
public static void main(String[] args) throws Exception {
for (String name : List.of("balancePence", "version", "owner", "id")) {
int mods = Account.class.getDeclaredField(name).getModifiers();
String level = Modifier.toString(mods);
System.out.println(name + " -> " + (level.isEmpty() ? "package-private" : level));
}
}
}
// Output:
// balancePence -> private
// version -> package-private
// owner -> protected
// id -> publicDefaults that are not package-private
Leaving out the modifier means different things in different places. In an interface it means public, in an enum constructor it means private, and record accessors are public whatever you write in the header.
import java.lang.reflect.Modifier;
public class Main {
interface Pricing {
long priceFor(String sku); // no modifier, still public
}
enum Status {
ACTIVE, SUSPENDED;
Status() {} // no modifier, always private
}
record Money(long pence) {} // generated accessor is public
public static void main(String[] args) throws Exception {
System.out.println(Modifier.toString(
Pricing.class.getMethod("priceFor", String.class).getModifiers()));
System.out.println(Modifier.toString(
Status.class.getDeclaredConstructor(String.class, int.class).getModifiers()));
System.out.println(Modifier.toString(
Money.class.getMethod("pence").getModifiers()));
}
}
// Output:
// public abstract
// private
// publicThe protected trap across packages
A subclass in another package can only reach protected members of objects that are known to be its own kind. Anything in package com.example.core, subclass or not, could use other.requestId freely.
// com/example/core/BaseHandler.java
package com.example.core;
public abstract class BaseHandler {
protected String requestId;
protected void log(String message) {
System.out.println(requestId + " " + message);
}
}
// com/example/web/OrderHandler.java
package com.example.web;
import com.example.core.BaseHandler;
public class OrderHandler extends BaseHandler {
void handle(OrderHandler sibling, BaseHandler other) {
log("handling"); // fine: inherited member, used through this
sibling.requestId = "r-1"; // fine: reference of our own type
other.requestId = "r-2"; // compile error: requestId has protected access in BaseHandler
}
}
Discussion