Encapsulation lets you change a class's internals without breaking the code that uses it.
Encapsulation means a class owns its data: fields are private, and the outside world interacts only through methods that keep the object valid. That way an invariant like "balance is never negative" is enforced in one place instead of hoped for everywhere.
Java has four access levels:
private— visible only inside the class (including its nested classes).- package-private (no keyword) — visible to classes in the same package.
protected— package plus subclasses in other packages.public— visible everywhere.
The rule of thumb: start with the most restrictive level that works, and widen only when needed. Fields are almost always private. Helper methods are private. Only the operations you are willing to support long-term become public.
Getters and setters are common, but don't generate a setter for every field by reflex. A method that expresses intent (account.deposit(amount)) is better than setBalance(getBalance() + amount) because it can validate and it documents the allowed operations. Fields that should never change after construction should be private final with no setter at all.
Be careful when exposing mutable internals. Returning your internal List lets callers modify it behind your back. Return an unmodifiable view or a copy instead, e.g. List.copyOf(items), and copy mutable arguments you store.
An object whose state can't change after construction is immutable. Immutable objects are simpler to reason about, can be shared freely, and are naturally thread-safe — String, LocalDate and records are all designed this way.
Example
import java.util.ArrayList;
import java.util.List;
public class Carts {
static class Cart {
private final List<String> items = new ArrayList<>();
private final int maxItems;
Cart(int maxItems) { this.maxItems = maxItems; }
public boolean add(String item) { // behaviour, with a rule
if (items.size() >= maxItems) return false;
return items.add(item);
}
public List<String> items() { return List.copyOf(items); } // callers can't modify ours
}
public static void main(String[] args) {
Cart cart = new Cart(2);
cart.add("pen");
cart.add("ink");
System.out.println(cart.add("pad")); // false: limit enforced
cart.items().add("hack"); // throws UnsupportedOperationException
}
}Remember
- Make fields
private; expose meaningful operations, not raw state - Default to the narrowest access level and widen only when needed
- Don't leak mutable internals — return copies or unmodifiable views
Watch out for
- Adding a public getter and setter for every field, which is barely better than a public field.