Encapsulation and Access Modifiers
Controlling visibility with public, private, and protected, and why hiding state behind getters and setters matters.
2 min read
Encapsulation is the practice of hiding an object's internal state and only exposing it through a controlled interface — methods you write, rather than direct field access. It's one of the four pillars of object-oriented programming, and in Java it's enforced with access modifiers.
The four access levels
| Modifier | Same class | Same package | Subclass (other package) | Everywhere |
|---|---|---|---|---|
| private | yes | no | no | no |
| (default, no modifier) | yes | yes | no | no |
| protected | yes | yes | yes | no |
| public | yes | yes | yes | yes |
private— visible only inside the declaring class. The tightest, and usually correct default for fields.- (no modifier, "package-private") — visible to anything in the same package.
protected— visible in the same package, plus subclasses anywhere.public— visible from anywhere.
Why hide fields at all
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit must be positive");
}
balance += amount;
}
}If balance were public, any code anywhere could do account.balance = -1000; and put the object into a nonsensical state, with no chance to validate or react. By making it private and only exposing deposit() and getBalance(), the class itself is the only code that can ever change balance directly — which means it's the only place that needs to enforce the "no negative deposits" rule.
Getters and setters
The conventional pattern for controlled access is a getter (returns a field's value) and, if mutation should be allowed at all, a setter (validates and updates it):
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be blank");
}
this.name = name;
}
}Not every field needs both — a value that should never change after construction should have a getter only, with no setter at all (or better yet, be declared final and set only in the constructor).
Encapsulation is about control, not just hiding
The point isn't secrecy for its own sake — it's that the class controls exactly how its own state can change, which means invariants (like "balance never goes negative") can actually be enforced in one place instead of hoped for everywhere the field is touched. This becomes essential once a codebase has more than one contributor: a well-encapsulated class can have its internals rewritten completely without breaking any code that only ever used its public methods.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.