Contents
- The Four Access Levels
- private — Maximum Encapsulation
- package-private — The Overlooked Default
- protected — Inheritance & Same Package
- public — Open to Everyone
- Access Modifiers on Classes
- Access Modifiers & the Module System
- API Design Rules
Java's four access modifiers form a spectrum from most restrictive to least restrictive. The table below summarises exactly who can see a member at each level.
| Modifier |
Same class |
Same package |
Subclass (other package) |
Everywhere else |
| private |
✅ | ❌ | ❌ | ❌ |
| package-private (no keyword) |
✅ | ✅ | ❌ | ❌ |
| protected |
✅ | ✅ | ✅ | ❌ |
| public |
✅ | ✅ | ✅ | ✅ |
The key insight is that access is determined at compile time, not runtime. The compiler checks whether the code that references a member is allowed to see it. If not, you get a compile error — not a runtime exception.
package com.example.accounts;
public class BankAccount {
private double balance; // only BankAccount sees this
double interestRate; // package-private — com.example.accounts only
protected String ownerName; // same package + subclasses
public String accountNumber; // everyone
public BankAccount(String number, String owner, double balance) {
this.accountNumber = number;
this.ownerName = owner;
this.balance = balance;
}
// Private helper — implementation detail, not part of the API
private void logTransaction(String type, double amount) {
System.out.printf("[%s] %s %.2f%n", accountNumber, type, amount);
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
balance += amount;
logTransaction("DEPOSIT", amount); // private method accessible here
}
public double getBalance() { return balance; }
}
When you write no modifier at all, Java uses package-private — not private, and not public. This surprises developers coming from languages where "no modifier" means public. Always be explicit about your intent.
private is the most restrictive level. A private member can only be accessed from within the same class — not from subclasses, not from other classes in the same package, not from anywhere else. This is the correct default for fields and implementation helpers. Keeping fields private and exposing them only through methods gives you full control to change the internal representation without breaking callers.
One subtlety: Java allows two instances of the same class to access each other's private members. This is used heavily in equals() implementations and copy constructors.
public class Money {
private final long cents; // private — callers work in dollars, we store cents
private final String currency;
public Money(double dollars, String currency) {
this.cents = Math.round(dollars * 100);
this.currency = currency;
}
public double getAmount() { return cents / 100.0; }
public String getCurrency() { return currency; }
// Two Money instances can access each other's private fields
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
Money result = new Money(0, currency);
// Direct field access to other.cents — legal because same class
((Money) result).cents = this.cents + other.cents; // conceptual
return new Money((this.cents + other.cents) / 100.0, currency);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Money)) return false;
Money other = (Money) obj;
// Access other.cents directly — perfectly legal, same class
return this.cents == other.cents && this.currency.equals(other.currency);
}
}
Never make fields non-private just to avoid writing getters. A public or package-private field turns an implementation detail into a public contract. Future refactoring (e.g., adding validation, lazy computation, or changing storage type) becomes impossible without breaking callers.
When you write no access modifier, Java applies package-private visibility. The member is visible to all classes in the same package but invisible outside it. This is the most underused and misunderstood access level — many developers use public when package-private would be correct, and many use private when they actually want collaboration within the package.
Package-private is ideal for classes, methods, and constructors that are part of an internal implementation but need to cooperate with other classes in the same package — such as helper classes, internal builders, or test hooks that you don't want to expose as part of the public API.
// File: com/example/http/RequestBuilder.java
package com.example.http;
// No modifier — package-private class. External code cannot instantiate it.
class RequestBuilder {
private String method;
private String url;
private final java.util.Map<String, String> headers = new java.util.LinkedHashMap<>();
private String body;
// Package-private constructor — only HttpClient (same package) can create one
RequestBuilder(String method, String url) {
this.method = method;
this.url = url;
}
RequestBuilder header(String name, String value) {
headers.put(name, value);
return this;
}
RequestBuilder body(String body) {
this.body = body;
return this;
}
// Package-private — visible to HttpClient, not to external callers
HttpRequest build() {
return new HttpRequest(method, url, headers, body);
}
}
// File: com/example/http/HttpClient.java
package com.example.http;
public class HttpClient {
// Public factory method is the entry point — RequestBuilder is an internal detail
public HttpRequest get(String url) {
return new RequestBuilder("GET", url)
.header("Accept", "application/json")
.build();
}
public HttpRequest post(String url, String json) {
return new RequestBuilder("POST", url)
.header("Content-Type", "application/json")
.body(json)
.build();
}
}
Package-private is the right access level for any class or member that is an implementation detail shared within a cohesive package but not meant for external consumers. Think of a package as a module boundary — what crosses that boundary should be public; what stays inside should be package-private or private.
protected is the access level that trips people up most. Its actual rule: a protected member is accessible from the same package (like package-private) AND from subclasses — even if those subclasses are in a different package. That "same package" part surprises developers who assume protected means "subclasses only".
There is one additional subtlety for subclasses in other packages: a subclass can only access a protected member through a reference of its own type or a subtype — not through a reference of the superclass type. This prevents a subclass from reaching into unrelated objects of the parent class.
// Package: com.example.base
package com.example.base;
public abstract class Shape {
// Protected — subclasses can read and write, same package too
protected String color = "black";
// Protected method — part of the extensible API for subclasses
protected void beforeDraw() {
System.out.println("Setting up context for " + color + " shape");
}
// Public template method — calls the protected hook
public final void draw() {
beforeDraw();
render();
System.out.println("Done.");
}
protected abstract void render();
}
// Package: com.example.shapes (different package!)
package com.example.shapes;
import com.example.base.Shape;
public class Circle extends Shape {
private final double radius;
public Circle(double radius) { this.radius = radius; }
@Override
protected void beforeDraw() {
// Can call and override the protected method from the superclass
super.beforeDraw();
System.out.println("Drawing circle with radius " + radius);
}
@Override
protected void render() {
System.out.println("● radius=" + radius + " color=" + color); // color is accessible
}
public void demonstrateProtectedAccess(Circle other) {
// Legal: accessing protected field through a Circle reference
System.out.println(other.color);
// ILLEGAL (would be a compile error): accessing through a Shape reference
// Shape s = other;
// System.out.println(s.color); // error — protected access through Shape
}
}
Use protected sparingly. Every protected member becomes part of your subclassing API — a contract you must honour for any subclass, including ones written by third parties. Changing or removing a protected member is a breaking change. Prefer private + a well-defined set of abstract or hook methods over exposing fields as protected.
public means the member is accessible from anywhere that can see the class. Public is the right choice for the parts of your class that constitute its intended API — the methods and constructors that users of your class should call. It is the wrong choice for fields (prefer getters), for implementation helpers (prefer private), and for internal collaborators (prefer package-private).
public class UserService {
// Private — storage is an implementation detail
private final java.util.Map<Long, User> store = new java.util.HashMap<>();
private long nextId = 1;
// Public — part of the service contract
public User create(String name, String email) {
validate(name, email); // private helper
User user = new User(nextId++, name, email);
store.put(user.getId(), user);
return user;
}
public java.util.Optional<User> findById(long id) {
return java.util.Optional.ofNullable(store.get(id));
}
public java.util.List<User> findAll() {
return java.util.List.copyOf(store.values());
}
public boolean delete(long id) {
return store.remove(id) != null;
}
// Private helper — not part of the public contract
private void validate(String name, String email) {
if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required");
if (email == null || !email.contains("@")) throw new IllegalArgumentException("Invalid email");
}
}
Public API is a commitment. Once you make something public — especially in a library — callers will depend on it. Removing or changing it is a breaking change. This is why the principle "make everything as private as possible, and promote visibility only when necessary" pays dividends over the lifetime of a codebase.
Top-level classes (those not nested inside another type) can only be public or package-private. A public top-level class must live in a file that matches its name. A package-private top-level class is an implementation class hidden from the package's consumers.
Nested classes (classes declared inside another class) can use all four access levels. The right choice depends on the relationship between the nested class and its enclosing class.
// Public top-level class — in file OrderProcessor.java
public class OrderProcessor {
// Private nested class — pure implementation detail
private static class ValidationResult {
final boolean valid;
final String message;
ValidationResult(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
}
// Package-private nested class — shared with other classes in the package
static class OrderMetrics {
int processed;
int failed;
}
// Public nested class (or interface) — part of the public API
public interface OrderListener {
void onSuccess(Order order);
void onFailure(Order order, String reason);
}
// Protected nested class — extensible by subclasses
protected static class BaseOrderHandler {
protected void handle(Order order) { /* ... */ }
}
private ValidationResult validate(Order order) {
if (order.getItems().isEmpty()) {
return new ValidationResult(false, "Order has no items");
}
return new ValidationResult(true, "OK");
}
public void process(Order order) {
ValidationResult result = validate(order);
if (!result.valid) throw new IllegalStateException(result.message);
// process...
}
}
Java 9 introduced the module system (JPMS), which adds a layer of encapsulation above packages. In a modular application, even a public class in a package that is not exports-ed in module-info.java is invisible to code in other modules. Modules let you truly hide internal packages that would otherwise be accessible via package-private or public members.
// module-info.java
module com.example.accounts {
// Only the api package is exported — internal packages stay hidden
exports com.example.accounts.api;
// NOT exported — internal implementation, inaccessible even if classes are public
// com.example.accounts.internal (hidden)
// com.example.accounts.repository (hidden)
}
// com/example/accounts/api/AccountService.java
package com.example.accounts.api;
public class AccountService { // public AND in an exported package → visible
public void openAccount() { /* ... */ }
}
// com/example/accounts/internal/AccountValidator.java
package com.example.accounts.internal;
public class AccountValidator { // public class, but package NOT exported
public boolean validate(Object o) { return true; } // invisible to other modules!
}
Before Java 9, access modifiers were the only encapsulation mechanism. Modules provide a second, coarser-grained layer: they control which packages are visible between modules. The combination gives you fine-grained control (private/package-private/protected/public) within a module and coarse-grained control (exports) at the module boundary.
A simple set of rules covers the vast majority of access-level decisions you will ever face:
| Member type | Default choice | Promote to… when |
| Instance fields |
private |
Never expose directly; use getters/setters |
| Helper methods |
private |
package-private if other classes in the package need it |
| API methods |
public |
Downgrade to package-private if only used internally |
| Extension hooks |
protected |
Only for members that subclasses must or should override |
| Top-level classes |
package-private |
public only when external callers need to reference the type |
| Constants |
public static final |
Only if truly part of the public API; otherwise private static final |
// Good example: minimal necessary visibility
public class EmailSender {
private static final String SMTP_HOST = "smtp.example.com"; // private constant
private static final int SMTP_PORT = 587;
private final javax.mail.Session session; // private field
public EmailSender() { // public constructor
this.session = createSession(); // private helper
}
public void send(String to, String subject, String body) { // public API
validateAddress(to); // private helper
deliver(createMessage(to, subject, body)); // private helpers
}
private javax.mail.Session createSession() { return null; /* ... */ }
private void validateAddress(String addr) { /* regex check */ }
private Object createMessage(String to, String subject, String body) { return null; }
private void deliver(Object msg) { /* SMTP send */ }
}
Resist the urge to make things public "just in case" someone needs them later. Public API is debt — once callers depend on something, you cannot take it away without breaking them. Start private and promote only under pressure.