Week 2: defensive copy, built from "who else can reach my list?"
By the end you can spot the moment a class hands out a reference to its own internals, close that leak on the way out and on the way in with a copy at the boundary, and say exactly when a shallow copy is enough and when it is not. This is the payoff of the Week 0 value-versus-reference idea: the aliasing bug you were warned about, now prevented on purpose.
The user story behind this. A department chair wants the Schedule they are building to hold its sections
in a way no other part of the system can quietly reach in and reorder or drop, so that a change made somewhere
else cannot corrupt the schedule without their knowing. A schedule that leaks its internal list costs the chair
a week hunting a change they never made.
Before you start
- [ ] I can explain that an object variable holds a reference, so two variables can share one object.
- [ ] I can write a class that composes another object as a field (last lesson).
- [ ] I can construct an
ArrayListfrom another collection,new ArrayList<>(other).
The problem (sit with this before reading on)
The Schedule owns a List<Course>, and it offers a getter so callers can read it:
public List<Course> courses() {
return courses; // hand back the field
}
A caller does something ordinary:
schedule.courses().clear();
The schedule is now empty, and no method of Schedule was ever called to empty it. The getter handed back a
reference to the one internal list, so the caller and the schedule were holding the same list the whole time.
Check yourself. No Schedule method removed anything, yet the schedule is empty. In one sentence that uses
the word reference, say what courses() actually gave the caller.
Atom 1: returning the field leaks the internals
A getter that returns a mutable field does not return the data; it returns a reference to the data. From that moment the caller can do anything to the field that the class itself can, including reorder it, clear it, or add a course that skipped every check the class makes. The class has lost control of its own state without a single one of its methods being at fault. Ownership from last lesson was only half the job; this is the half that protects it.
Check yourself. A method returns this.sections directly. Name one thing a caller could do to the returned
list that the class never intended.
Atom 2: copy on the way out
The fix is to hand back a copy, not the field. The caller gets a list of the same courses, but it is their own list, so anything they do to it leaves the schedule untouched:
public List<Course> courses() {
return new ArrayList<>(courses); // a fresh list, same elements
}
Now schedule.courses().clear() clears the caller's throwaway copy and the schedule keeps its courses. This is
a defensive copy on the way out: the boundary of the object is where you copy, so no reference to the internals
ever escapes.
Check yourself. With the copy-on-the-way-out getter, predict what schedule.courses().clear() does to the
schedule, and why.
Atom 3: copy on the way in, too
The leak has a second door. If the constructor stores the list it was handed, the caller who passed it still holds a reference to that same list and can mutate it later, reaching into the schedule from the outside:
public Schedule(List<Course> courses) {
this.courses = courses; // stores the caller's list
}
The caller keeps their reference, adds a course next week, and the schedule changes with it. The fix is the same move at the other boundary, copy on the way in:
public Schedule(List<Course> courses) {
this.courses = new ArrayList<>(courses); // the schedule's own list
}
A class that copies at both boundaries, in and out, owns a list no outside reference can reach. That is the whole technique.
Check yourself. A constructor does this.courses = courses. In one sentence, how can the caller change the
schedule's courses a week later without calling any Schedule method?
Atom 4: when a shallow copy is enough
A copy of the list copies the references inside it, not the Course objects themselves. The new list points at
the same courses. That is exactly right here for one reason: a Course is immutable, built once and never
changed, so sharing the course objects is safe. No one can mutate a shared Course, because a Course has no
mutating method.
The honest limit: if the elements were mutable, a copy of the list alone would not protect them, because two lists would still point at the same changeable objects. Then you would copy the elements too, a deep copy. You copy as deep as the mutability goes, and no deeper. For an immutable element, the list copy is the whole job.
Check yourself. The list copy shares the Course objects. In one sentence, why is that safe for Course
but would not be safe if Course had a setCredits method?
What you collected
- A getter that returns a mutable field returns a reference to the internals, not a safe copy.
- Defensive copy means copying at the boundary, on the way out and on the way in, so no outside reference can reach the field.
- A shallow copy of a list shares its elements, which is correct exactly when the elements are immutable.
- This is the value-versus-reference idea from Week 0 paid off: the aliasing bug, prevented by design.
Where this is going
You now have a Schedule that owns and protects a list of courses. Next is the question of what kinds of values
those fields are allowed to hold at all: a term is one of a fixed set, a campus is one of a fixed set, and a
typo should be a compile error, not a runtime surprise. That is enums, the next lesson.