Java Encapsulation
›Short versionOpen if you only have a minute.
- Encapsulation means other classes change an object only through its methods, so the class can keep its invariants.
- Private fields are the usual mechanism. Public methods are the API callers are allowed to depend on.
- A getter reports state. A setter is optional. Prefer deposit and withdraw when a raw setBalance would skip the rules.
- Returning a live internal list lets callers mutate it behind your methods. Return a copy or an unmodifiable view.
- If equals compares a set of fields, hashCode must use that same set. Equal objects then share a hash bucket.
- Do not put a mutable equality field on an object that will be a hash key. The bucket is chosen at insertion.
The balance on the screen is 80. Another class, written by someone who needed a quick adjustment, sets the field to -40. Every method that believed "the balance is never negative" is now reading a number the account itself would have refused. is how the account stays the only writer.
The methods are the door
The here is simple: balance is never negative when a public method returns. The field is private, so the only assignments are in this file.
public final class Account {
private final String id;
private int balance;
public Account(String id, int opening) {
if (id == null || id.isBlank() || opening < 0) {
throw new IllegalArgumentException();
}
this.id = id;
this.balance = opening;
}
public void deposit(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("amount");
}
balance += amount;
}
public void withdraw(int amount) {
if (amount <= 0 || amount > balance) {
throw new IllegalArgumentException("amount");
}
balance -= amount;
}
public int balance() {
return balance;
}
public String id() {
return id;
}
}balance() is a . There is no setBalance. Deposit and withdraw are the changes that make sense, and each one can say no. An id is fixed, so it is a final field with a reader and nothing that writes it again. Getters and setters for every field would republish the representation and call it encapsulation. The is the set of operations, not a mirror of the fields.
A list that leaks
A private list stops being private the moment a method returns it. The caller holds the same list the object uses for its .
public List<String> tags() {
return tags; // caller can tags().clear()
}
public List<String> tags() {
return List.copyOf(tags);
}List.copyOf is a . Clearing the returned list fails. The account, or the customer record, keeps the list it checked when the tags were added. If callers need a snapshot they can edit, copy on the way out and document that the copy is theirs. Copy on the way in when the constructor receives a list the caller will keep using.
Equality uses the hidden state
Callers compare accounts by id because that is the value, even though the field is private. are ordinary methods. They may read the private fields. They have to agree with each other.
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof Account account)) {
return false;
}
return id.equals(account.id);
}
@Override
public int hashCode() {
return id.hashCode();
}Balance is left out of both methods. Two snapshots of the same account compare equal after a deposit, which is what you want if the id is the identity of the value. If balance were included, a deposit would change the hash code and a HashMap keyed by the account would miss it on the next lookup. The class is final, so the instanceof check will not treat a subclass with extra fields as equal in one direction and unequal in the other.
$ javac Account.java$ java Account
tags() returns the ArrayList the object stores. A caller calls tags().add("vip"). Did addTag run?
No. add ran on the list. Any check, log, or invariant inside addTag was skipped. Return List.copyOf(tags) from the getter, and keep mutation on a method of the object.