When we first learn Object-Oriented Programming, encapsulation is often explained like this:
Make fields
privateand access them using getters and setters.
Technically, this introduces data hiding, but it does not automatically give us good encapsulation.
There is an important difference.
class BankAccount {
private double balance;
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
At first glance, this looks perfectly encapsulated.
The field is private.
Nobody can directly write:
account.balance = 1000;
Instead, they have to call:
account.setBalance(1000);
But ask yourself one question:
What exactly did we protect?
Not much.
A caller can still do this:
account.setBalance(-50000);
or:
account.setBalance(999999999);
The field is private, but the object's internal state is still completely controlled by the outside world.
That is not strong encapsulation.
What Encapsulation Actually Means
Encapsulation is not just about restricting how a variable is accessed.
It is about restricting how an object's state can change.
A well-encapsulated object should:
- protect its internal state
- enforce business rules
- maintain valid state
- expose meaningful operations
- hide unnecessary implementation details
The object itself should decide which state transitions are allowed.
This leads to an important concept:
Invariants
An invariant is a condition that should always remain true for an object.
For example, suppose our banking system does not allow a balance to become negative.
Then:
balance >= 0
is an invariant.
If we expose a generic setter:
setBalance(double balance)
we are allowing outside code to break that invariant.
Instead of exposing the state directly, we should expose behavior.
A Better Design
Consider this version:
class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
if (initialBalance < 0) {
throw new IllegalArgumentException(
"Initial balance cannot be negative"
);
}
this.balance = initialBalance;
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException(
"Deposit amount must be positive"
);
}
balance += amount;
}
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException(
"Withdrawal amount must be positive"
);
}
if (amount > balance) {
throw new IllegalStateException(
"Insufficient balance"
);
}
balance -= amount;
}
}
Notice something important.
There is no setBalance() method.
Instead, the object exposes operations that make sense in its domain:
deposit()
withdraw()
getBalance()
Now someone cannot arbitrarily change:
1000 -> 500000
by calling:
setBalance(500000);
They have to perform a legitimate operation.
account.deposit(500);
This is much closer to real encapsulation.
Data Hiding vs Encapsulation
These two concepts are related, but they are not identical.