skip to content

Why can a getter or setter that exposes a mutable internal object break information hiding, and how do you fix it?

level: middleimportance: must knowfreq 60%

answer

  1. private stops reassignment, not mutation of the pointed-to object
  2. reference leak via getter/setter
  3. defensive copy in AND out
  4. List.copyOf / unmodifiableList / Arrays.copyOf
  5. immutable types make leaks impossible

basics

~20 s

If a getter returns the actual internal mutable object (like a List or Date), callers can change it from outside, bypassing your class's rules. The fix is to return or store a copy (a defensive copy), or hand back an unmodifiable view, so the internal state stays under the class's control.

solid answer

~40 s

Making a field private isn't enough if the getter hands out a reference to a mutable internal object — the caller can mutate it directly and skip your validation, silently breaking your invariants. This is reference leaking. The classic cases are collections (List, Map), arrays, and old mutable types like java.util.Date. The fixes: in setters, store a defensive copy of the incoming object so later external changes don't reach your field; in getters, return a copy or an unmodifiable view (Collections.unmodifiableList, List.copyOf) so callers can't mutate your state. Even better, prefer immutable types (records of immutable components, java.time, List.copyOf) so there's nothing mutable to leak. The principle: information hiding is about controlling state, and a leaked reference to mutable state is an uncontrolled back door regardless of the private keyword.

code

java · 19 lines
java
public final class Schedule {
    private final List<String> slots;

    public Schedule(List<String> slots) {
        // defensive copy IN: independent of the caller's list
        this.slots = new ArrayList<>(slots);
    }

    public List<String> getSlots() {
        // copy OUT: caller cannot mutate our state
        return List.copyOf(slots);
    }

    public void addSlot(String slot) {
        if (slot == null || slot.isBlank())
            throw new IllegalArgumentException("blank slot");
        slots.add(slot); // the only controlled write path
    }
}

go deeper

for a junior

Recognizes that a getter can hand out the real internal object and that returning a copy is safer; may not yet cover input-side copying or arrays.

for a middle

Identifies reference leaking in both getters and setters, applies defensive copies / unmodifiable views correctly, and knows arrays and java.util.Date are common offenders.

for a senior

Argues for immutable-by-default design to eliminate the bug class, weighs copy cost vs view, and connects leaking to thread-safety and aliasing.

for a principal

Sets team conventions (java.time over Date, List.of/records, immutability checklist), reasons about performance trade-offs of defensive copying in hot paths, and audits APIs for escape of internal state across module boundaries.

## The trap: private isn't the whole story Beginners learn "make fields private and add a getter" and assume the state is now protected. But in Java, a getter that **returns the field** returns a **reference** to the same object the class holds. If that object is **mutable** (can be changed in place), the caller now has a handle to your internal state and can modify it without ever calling your class. The `private` keyword stopped them from *reassigning* the field, but not from *mutating the object the field points to*. This is called a **reference leak** (or letting state "escape"). ### Key terms - **Mutable object:** an object whose contents can change after creation, e.g. `ArrayList` (add/remove), a primitive array, or the legacy `java.util.Date`. - **Immutable object:** one whose state never changes after construction, e.g. `String`, `Integer`, `java.time.LocalDate`, or a list made with `List.copyOf`. - **Reference:** in Java, object variables hold a reference (a pointer) to the object, not the object itself. Returning the field copies the reference, not the object. - **Defensive copy:** making a fresh, independent copy of a mutable object on the way in (in a constructor/setter) and/or on the way out (in a getter), so the internal and external copies can't affect each other. ## A broken example ```java public class Schedule { private List<String> slots = new ArrayList<>(); public List<String> getSlots() { return slots; } // leaks the real list public void setSlots(List<String> slots) { this.slots = slots; } // stores caller's list } ``` Problems: ```java Schedule s = new Schedule(); s.getSlots().add("hack"); // mutated internal state, no validation ran List<String> external = new ArrayList<>(); s.setSlots(external); external.add("sneaky"); // external list IS the internal one now ``` Both the getter and the setter share the *same list object* with the outside world. Any invariant `Schedule` wanted to enforce (max slots, no nulls, etc.) is bypassed. ## The fixes ### 1. Defensive copy on input (constructor/setter) ```java public void setSlots(List<String> slots) { this.slots = new ArrayList<>(slots); // copy: caller's later edits don't reach us } ``` Now the field points to *our* list, independent of the caller's. ### 2. Defensive copy or unmodifiable view on output (getter) ```java public List<String> getSlots() { return List.copyOf(slots); // independent immutable snapshot // or: return Collections.unmodifiableList(slots); // live read-only view } ``` `List.copyOf` returns an immutable copy — mutations throw and don't touch the field. `Collections.unmodifiableList` returns a *view*: read-only to the caller, but it still reflects later internal changes (and the caller can't mutate it). ### 3. Best: use immutable types so there's nothing to leak If the field is already immutable (`List.copyOf(...)` stored once, `LocalDate` instead of `Date`, a record of immutable components), you can return it directly with zero copying because no one can mutate it. This is why modern Java favors `java.time`, `List.of`/`Map.of`, records, and final immutable fields — they make this whole class of bug impossible. ## Arrays are the sharpest edge Arrays are always mutable and have no read-only view, so a `getData()` returning the array, or storing the passed-in array, both leak. You must `clone()` or `Arrays.copyOf` on the way in and out. ## Why this is a hiding question, not just a bug Information hiding's whole point is that the class *controls* its state. A leaked mutable reference is an uncontrolled write path into private state — it defeats validation, breaks invariants, and can cause subtle aliasing bugs and thread-safety problems (two parties mutating the same object). So "private field + naive getter/setter" only *looks* encapsulated. Real hiding requires controlling not just access to the field but access to the *object the field points to*. ## Trade-off Defensive copying costs allocation and CPU on every call. For hot paths or large structures you may instead document immutability expectations, return an unmodifiable view (cheaper than a copy), or design with immutable types from the start so copying is unnecessary.

  • What's the difference between returning Collections.unmodifiableList(list) and List.copyOf(list) from a getter?
    unmodifiableList returns a read-only VIEW backed by the original list, so it reflects later internal changes but the caller can't mutate it. List.copyOf returns an independent immutable SNAPSHOT that won't see later internal changes. Both stop the caller from mutating your state; choose based on whether the caller should observe live updates.
  • How do records help with this problem?
    A record auto-generates accessors and is shallowly immutable in its references, but if a component is a mutable type (e.g. a List), the accessor still leaks it. You fix it by defensively copying in the compact constructor (e.g. List.copyOf) so the stored component is itself immutable.

saying these in an interview costs you the question

  • Thinking a private field with a plain getter is fully encapsulated even when the field is a mutable collection/array/Date.
  • Copying only on the way out but storing the caller's object on the way in (or vice versa) — both directions can leak.
  • Confusing Collections.unmodifiableList (a live read-only view) with List.copyOf (an independent immutable snapshot).
  • Forgetting arrays have no read-only view, so they must be cloned/copied.

context