CSCD 211 › lessons

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

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

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.