Contents

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 typeDefault choicePromote 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.