Encapsulation
Hiding internal state behind a small, deliberate interface — and why that one habit is the foundation of every other OOP idea
Encapsulation is the practice of hiding an object's internal state and only allowing the world to interact with it through a small, deliberate set of methods. It is the first and most important discipline of OOP. Everything else in this course — inheritance, polymorphism, composition, design patterns — only pays off if your objects are well-encapsulated.
The word itself is a metaphor. A capsule has a shell that protects what is inside. From the outside you can swallow the capsule, but you cannot reach the powder directly. An object is the same: it has a shell of methods, and the data inside is none of your business.
Why hide state at all?
Recall the procedural pain from The Software Crisis: when any function can write to any data, bugs become impossible to localize and rules become impossible to enforce. Encapsulation is the direct fix.
All access to the balance goes through the methods. Inside deposit
and withdraw, the class can enforce its rules — no negative deposits,
no overdrafts — once, in one place.
The two failure modes
Look at this anti-example and notice every way it can go wrong.
There are two problems here, and they are different:
- No invariants are enforced. Negative balances are illegal in
our domain, but
OpenAccountdoesn't say so. Every caller has to remember the rule. - The internal representation is part of the public API. If
tomorrow we decide to store the balance in cents (as an
int) instead of dollars (as adouble), every caller breaks.
Encapsulation fixes both at once.
Now the rule "balance is never negative" is guaranteed by the
class. There is no path through the public API that can break it.
And the internal representation (double balance) can change without
breaking a single caller, because callers never see it.
Access modifiers in Java
Java has four access levels that control where a member can be seen
from. You will spend 95% of your time using private and public,
but it is worth knowing all four.
| Modifier | Visible from |
|---|---|
private | Same class only |
| (no modifier) | Same package only (called package-private) |
protected | Same package, and subclasses anywhere |
public | Anywhere |
The default discipline is: make everything as private as you can,
and widen only when a real need appears. A field should be private
unless you have a specific reason. A helper method that is only called
from inside the class should be private. Constructors are often
public but can be private if you want to force construction
through a factory method.
Getters, setters, and the temptation to over-expose
A getter is a method that lets the outside world read a value
(public double balance()). A setter is a method that lets the
outside world write a value (public void setBalance(double)).
A common beginner mistake is to write a getter and a setter for every single field, and call that "encapsulation." It isn't. It mostly just makes your fields public with extra typing.
A BankAccount should not have setBalance(double). That throws
away the whole point. It should have deposit and withdraw,
methods named after what the world is actually allowed to do. The
right question to ask when designing a class is "what should
callers be able to do?" — not "what fields does it have?".
`final` fields lean on encapsulation
private final is a very common combination: the field is invisible
to outsiders, and once the constructor sets it, the class itself
cannot change it either. This makes the object's identity-defining
data effectively immutable, which removes whole classes of bug.
A bigger encapsulation example
Let's encapsulate a slightly richer object: a Thermostat with an
allowed temperature range and a current setting. Outsiders can
request a new setting, but the thermostat enforces its own limits.
Three things to notice:
Thermostathas no setter that lets the caller bypassclamp. The only way to changetargetis throughsetTarget, which always clamps.clampisprivatebecause it is internal plumbing. Callers don't need it and shouldn't see it.minFandmaxFarefinal. The thermostat's range is part of its identity and never changes after construction.
That is a fully encapsulated object. From the outside it looks tiny; inside it protects a clear invariant.
Practice: encapsulate this!
Implement a Wallet class.
Requirements:
- A
privatefieldcentsthat starts at0. add(int cents)increases the balance. It must throwIllegalArgumentExceptionifcents <= 0.spend(int cents)returnstrueand decreases the balance if there is enough; otherwise it returnsfalseand does not change the balance. It must also throwIllegalArgumentExceptionifcents <= 0.balance()returns the current balance.
Main.java runs a fixed scenario and is expected to print exactly:
spend1=true
spend2=false
balance=20
Test your understanding
Why is it a bad idea to make a field public?
Public fields take up more memory
Public fields cannot be read
Any code anywhere can modify the field, so the class cannot enforce its own invariants
The compiler refuses to compile a class with public fields
Which is the best sign that a class is well-encapsulated?
It has a getter and setter for every field
Its constructors throw exceptions on invalid input
Its public methods are named for behaviors callers actually need, and its fields are private
It uses the keyword final somewhere
In the Thermostat example, why is clamp declared private?
Because Java requires helper methods to be private
Because private methods run faster
Because it is internal plumbing that callers never need to see, and hiding it keeps the public API small and intentional
Because public methods cannot call private ones