What Are Access Modifiers?
An access modifier is a keyword that controls which parts of your
code can see and use a class, method, or field. Java provides four levels:
private, package-private (no keyword), protected, and
public. Together they are the primary tool for implementing
encapsulation — one of the four pillars of OOP.
The problem they solve: in any non-trivial program, not everything should be accessible from everywhere. If every field and method is public, any part of the codebase can read or modify internal state directly — making it impossible to change the implementation without breaking every caller, and impossible to guarantee that an object is always in a valid state.
// Without access control: anyone can corrupt state
public class User {
public String password; // anyone can read or overwrite this
public int age; // anyone can set age = -999
}
user.password = "plaintext"; // no hashing, no validation, no control
user.age = -999; // completely invalid, silently accepted
// With access control: the class enforces its own rules
public class User {
private String passwordHash; // internal detail, hidden
private int age;
public void setPassword(String raw) {
this.passwordHash = hash(raw); // always hashed, no exceptions
}
public void setAge(int age) {
if (age < 0) throw new IllegalArgumentException("Invalid age");
this.age = age;
}
}
Every class, method, and field should have the minimum visibility
it needs to function. Start with private and widen
only when something external genuinely requires access. Every time you make
something public, you are making a promise to every caller that
this API will remain stable. Narrow visibility = fewer promises = more
freedom to refactor.
The Four Access Levels
Access modifiers are the primary tool for encapsulation in Java — they
control what parts of your code are visible to other classes. The rule is simple:
start with private and widen only when something genuinely needs it.
Every unnecessary public method or field is a commitment you'll have to maintain forever.
| Modifier | Same class | Same package | Subclass (other pkg) | Everywhere |
|---|---|---|---|---|
private |
✅ | ❌ | ❌ | ❌ |
| (package-private) | ✅ | ✅ | ❌ | ❌ |
protected |
✅ | ✅ | ✅ * | ❌ |
public |
✅ | ✅ | ✅ | ✅ |
* Protected access in a subclass from a different package only works through
inheritance (this/super), not on arbitrary instances of the
parent class.
Top-level classes: only public or package-private. A private top-level
class would be completely unreachable — the compiler rejects it.
Nested classes, constructors, methods, fields: all four modifiers.
Interface methods: implicitly public (unless default,
static, or private — private interface methods added
in Java 9).
Each Modifier in Detail
private — the default you should reach for
public class BankAccount {
// Fields: always private — never expose raw state
private String accountNumber;
private double balance;
private String pin;
// Public API: controlled operations with validation
public double getBalance() { return balance; }
public boolean withdraw(double amount, String enteredPin) {
if (!validatePin(enteredPin) || amount <= 0 || amount > balance) return false;
balance -= amount;
logTransaction("WITHDRAW", amount);
return true;
}
// Private helpers: implementation detail, free to change anytime
private boolean validatePin(String p) { return this.pin.equals(p); }
private void logTransaction(String type, double amount) { ... }
}
// ❌ These are compile errors from any other class:
// account.balance = 1_000_000; private access
// account.validatePin("0000"); private access
package-private (no modifier) — internal implementation
No keyword needed — just omit the modifier. Useful for implementation classes that belong to a module but shouldn't be part of its public API.
// com/app/internal/UserRepository.java
package com.app.internal;
class UserRepository { // ← no modifier = package-private
User findById(long id) { ... }
void save(User user) { ... }
}
// com/app/service/UserService.java — DIFFERENT package
import com.app.internal.UserRepository; // ❌ ERROR: not visible
/*
* com.mylib
* ├── api/ ← public classes only — this IS your API
* │ ├── UserService.java (public)
* │ └── User.java (public)
* └── internal/ ← package-private classes — implementation detail
* ├── UserRepository.java (package-private)
* └── CacheManager.java (package-private)
*
* Library users can only see api/. You can refactor internal/ freely.
*/
protected — for inheritance extension points
protected means same-package access PLUS subclasses in any package.
The key nuance: subclass access only works via this/super,
not on an arbitrary parent instance.
// package: com.app.model
public class Animal {
protected String name; // accessible in subclasses
private String internalId; // NOT accessible even in subclasses
protected void makeSound() { } // override hook
}
// package: com.app.pets (DIFFERENT package)
public class Dog extends Animal {
@Override
protected void makeSound() {
System.out.println(name + " barks"); // ✅ protected field via 'this'
}
void weirdAccess(Animal other) {
// other.name = "x"; ❌ NOT allowed — different pkg, not via inheritance
this.name = "Rex"; // ✅ allowed — own inherited field
}
}
public — your API contract
public class UserService {
public static final int MAX_USERNAME_LENGTH = 50; // OK: constant
public User createUser(String username, String email) { ... }
public Optional<User> findByUsername(String username) { ... }
public void deleteUser(long id) { ... }
// These are private — users of UserService don't need to know about them
private void validateInput(String input) { ... }
private void saveToDatabase(User user) { ... }
}
// ❌ Anyone can set password to anything — no validation possible
public class User { public String password; }
// ✅ Controlled: validation lives in one place, changeable without breaking API
public class User {
private String passwordHash;
public void setPassword(String raw) {
if (raw.length() < 8) throw new IllegalArgumentException("Too short");
this.passwordHash = hash(raw);
}
}
// ✅ Exception: public static final constants are fine
public static final int MAX_SIZE = 100;
Private Constructors
Two canonical patterns use private constructors to control instantiation:
// PATTERN 1: Utility class — no instances allowed
public final class MathUtils {
private MathUtils() {
throw new AssertionError("Utility class");
}
public static int square(int n) { return n * n; }
}
// PATTERN 2: Singleton — exactly one instance
public class AppConfig {
private static final AppConfig INSTANCE = new AppConfig();
private AppConfig() { } // nobody else can instantiate
public static AppConfig getInstance() { return INSTANCE; }
}
// Note: in Spring/CDI apps, singletons are managed by the container — you
// rarely write Singleton pattern manually in modern enterprise Java.
Encapsulation in Practice
Defensive copies for mutable fields
// ❌ BAD: caller can mutate your internal state
public class Team {
private List<Player> players = new ArrayList<>();
public List<Player> getPlayers() { return players; }
}
team.getPlayers().clear(); // 💥 wipes the team's roster
// ✅ GOOD option 1: unmodifiable view (no copy overhead)
public List<Player> getPlayers() {
return Collections.unmodifiableList(players);
}
// ✅ GOOD option 2: defensive copy (caller gets independent list)
public List<Player> getPlayers() {
return new ArrayList<>(players);
}
// ✅ MODERN: return immutable copy (Java 10+)
public List<Player> getPlayers() {
return List.copyOf(players);
}
The principle of least privilege — applied
public class OrderProcessor {
// Fields: always private
private final OrderRepository repository;
private final EmailService emailService;
// Constructor: public — callers need to build it
public OrderProcessor(OrderRepository repo, EmailService email) {
this.repository = repo;
this.emailService = email;
}
// Public API: what callers are allowed to do
public Order process(OrderRequest request) {
validate(request);
Order order = buildOrder(request);
repository.save(order);
emailService.sendConfirmation(order);
return order;
}
// Protected: extension hook for subclasses
protected void validate(OrderRequest r) {
if (r.quantity() <= 0) throw new IllegalArgumentException("Bad qty");
}
// Private: internal wiring nobody else needs to see
private Order buildOrder(OrderRequest r) {
return new Order(r.productId(), r.quantity(), LocalDateTime.now());
}
}
- Instance fields →
private. Always. - Helper methods →
private - Extension hooks for subclasses →
protected - Internal implementation classes → package-private
- Public API methods / constants →
public - If unsure → start
private, promote later
Java 9+ Modules: Access Beyond Packages
The Java Platform Module System (JPMS), introduced in Java 9, adds a layer of access
control above packages. A module explicitly declares what it exports — even
public classes in unexported packages are inaccessible to other modules.
// module-info.java
module com.mylib {
exports com.mylib.api; // only api package is accessible
// com.mylib.internal is NOT exported — invisible to all other modules
// even if classes inside are public
}
// Another module:
module com.myapp {
requires com.mylib;
// Can use com.mylib.api.UserService (public + exported)
// Cannot use com.mylib.internal.UserRepository (not exported)
}
Pre-Java 9, setAccessible(true) could bypass any access modifier —
useful for frameworks (Spring, Hibernate) that need to inject fields or proxy classes.
With modules, this is restricted by default: a module must explicitly
opens a package to allow deep reflection. This is why Spring Boot
and Hibernate require specific --add-opens JVM flags or
opens declarations when used in a modular application.
Interview Questions
Q: What are the four access modifiers and their scope?
private — declaring class only. Package-private (no keyword) — same
package. protected — same package plus subclasses in any package.
public — everywhere. "default" is not a keyword; it's the absence of one.
Q: Can a top-level class be private?
No. Top-level classes can only be public or package-private. A private
top-level class would be unreachable — the compiler rejects it. Nested classes can
be private.
Q: Why should instance variables almost always be private?
Encapsulation: private fields let you change the internal representation without
breaking any code that uses the class. If you expose a field directly, any change
to its type or name breaks all callers. With a private field and a getter, you
can change the implementation while keeping the API stable.
Q: What is the protected access nuance that trips people up?
In a subclass from a different package, you can access a protected member only
through this or super — not through an arbitrary reference
to the parent class. Example: inside Dog extends Animal, you can
read this.name but not someOtherAnimal.name, even though
both are Animal. This is the spec — the JLS specifies it explicitly.
Q: Can an overriding method have more restrictive access?
No. The Liskov Substitution Principle requires that a subtype be substitutable for
its supertype. If you could narrow public to private,
code holding a reference to the supertype would break at runtime. The compiler
enforces this: overriding methods must have equal or wider access.
Q: How does the Java 9 module system change access control?
Modules add a layer above packages: even public classes in unexported
packages are inaccessible to other modules. This closed-by-default model is why
setAccessible(true) via reflection is now restricted — a module must
explicitly opens a package for deep reflection. This broke many
frameworks (Spring, Hibernate, Jackson) during the Java 9 migration, requiring
--add-opens flags as a temporary bridge.
Q: When would you use package-private instead of private?
When you need access from test classes in the same package, or when related classes
in a package need to collaborate on internals without exposing that to the world.
Example: a UserRepository used only by UserService in
the same package. Making it package-private keeps it invisible to the rest of the
application while avoiding the overhead of a formal API.