Card 05/ 08
Defensive Copies: The Same Leak Through a Constructor
Every getter returns a copy, and the log still gains entries nobody added.
A constructor that stores a list it was handed keeps the caller's address, so the caller can still change the object's list afterwards. The fix is a defensive copy: copy the list on the way in, as well as on the way out.
Every getter now returns a copy. The audit log still gains entries nobody added, and this time the class never hands anything out at all:
class Audit {
private final List<String> entries;
Audit(List<String> initial) {
this.entries = initial;
}
List<String> entries() { return List.copyOf(entries); }
}List<String> mine = new ArrayList<>();
Audit audit = new Audit(mine);
mine.add(""); // and the audit has it tooLine 5 stored the address it was handed. The caller kept their own copy of that address, so both the caller and the object now reach one list, and the validation in addEntry never sees anything the caller does directly.
Put this beside a getter that returns the private list itself, which leaks it the other way, and the shared fact is the whole lesson: private protects the variable, never the object it points at, and it does not matter which direction the reference crossed the boundary. Out through a getter or in through a constructor, the result is two names for one object.
Audit(List<String> initial) {
this.entries = new ArrayList<>(initial);
}None of this applies to a field holding a String, an int or any other immutable value. There is nothing to reach back into, which is why immutable types, ones that cannot change after creation, make encapsulation, keeping a class's data under its own control, cheap.
← → move through sections and cards