Encapsulation and Abstraction
The two ideas that turn a collection of classes into a maintainable program — hide what callers should not see, expose what they need
We have been writing private fields and public methods without fully explaining why. Now we explain. The two ideas at work are encapsulation and abstraction, and they are the reason OOP exists at all.
Encapsulation: each object owns its data
Encapsulation is the rule that only an object's own methods may touch its own data. Outside code asks the object to do things; it does not reach in and edit fields.
Mechanically, Java enforces this with access modifiers:
| Modifier | Visible to |
|---|---|
private | Only code inside the same class |
| (package-private, no modifier) | Code in the same package |
protected | Same package, plus subclasses |
public | Everyone |
The single most important habit you can build right now: make
fields private by default, and only expose what callers need
through methods.
Why does this matter?
Imagine balanceCents were public. Anywhere in your codebase,
someone could write account.balanceCents = -1_000_000; — and the
account class would have no way to enforce the rule "a balance
cannot be negative."
With balanceCents private and only deposit and withdraw
exposed, the class owns the rules. The class can refuse a
withdrawal that would overdraw the account. The class can log every
change. The class is the single place where decisions about its
own data live. If the balance is wrong, you know exactly where to
look.
This is the technical foundation that made it possible to write million-line software systems without going insane.
A worked example
Notice how much logic lives inside BankAccount itself. The class
refuses negative or zero amounts. It refuses overdraws. Callers
cannot bypass these rules because they cannot touch
balanceCents directly.
If next month we add a "fees" feature, all the logic lives in
BankAccount. The rest of the program stays the same.
Abstraction: callers see the what, not the how
Abstraction is the design idea behind encapsulation: callers care about what an object does for them, not how it does it.
The BankAccount class is, from the outside, just:
- "I have an owner."
- "I have a balance."
- "You can deposit money into me."
- "You can ask to withdraw — I'll say yes or no."
That is the abstraction. The fact that the balance is stored as
balanceCents (rather than as balanceDollars or as a BigDecimal)
is the implementation. We can change the implementation without
breaking any caller, as long as the abstraction stays the same.
A class is well designed to the extent that its abstraction is small, clean, and stable while its implementation can change freely.
Common mistakes beginners make
1. Public fields "to save time"
public class Person {
public String name;
public int age;
}This skips encapsulation entirely. It is a struct masquerading as
a class. There is no way for Person to enforce rules about its own
state. Whenever you see this, ask: who is allowed to set age = -5?
2. Getters and setters for every field
The opposite extreme is almost as bad. If you write
getX/setX for every private field, you have exposed the
implementation through a thin disguise. Outside code can still set
the field to nonsense, just through a method.
// Don't blindly do this:
public void setAge(int age) { this.age = age; }A good rule: do not add a setter unless your design genuinely
requires the field to change after construction. If it does, the
setter should enforce the rules. ("setAge must be > 0.")
3. Methods that expose internal collections
public List<Item> getItems() {
return items; // returns the internal list — outside code can mutate it!
}Callers can now do account.getItems().clear() and silently break
the class's invariants. Common fixes: return a defensive copy, or
return an unmodifiable view, or expose specific operations like
add, remove, count instead.
What is the single most important reason to make fields private?
It uses less memory
It makes the class faster
So the class itself is the only place that decides how its own data may change, allowing it to enforce its invariants
Because the Java compiler refuses to compile public fields
Which is the best sign that a class is well-encapsulated?
It has a public field for every state value
It has a getter and setter for every field
Its public methods are named for behaviors that callers actually need, and its fields are private
It uses the keyword final somewhere
A small challenge
Build a class Thermostat that:
- Has a private
intfieldtargetCfor the target temperature in Celsius. - Has a constructor
Thermostat(int initialTarget). - Has a method
int target()that returns the current target. - Has a method
void setTarget(int newTarget)that updates the target only if it is between 5 and 35 inclusive; otherwise the method does nothing and the target stays the same.
Main.java is given and must print exactly:
target=20
after legal change: 24
after illegal change: 24
Notice how setTarget silently refuses a bad value. The
abstraction is "this thermostat is always sensible." Implementation
details — bounds checks, clamping, perhaps logging — stay inside.
Next we look at constructors in more depth: how an object comes to exist, and the entire lifecycle from birth to garbage collection.