Question 29 · Section 13

How to properly work with collections in immutable classes?

Collections are the most complex part of immutable classes. You need to protect the collection at every stage.

Language versions: English Russian Ukrainian

Junior Level

Collections are the most complex part of immutable classes. You need to protect the collection at every stage.

Three rules

1. Copy on input (constructor)

public final class Group {
    private final List<String> members;

    public Group(List<String> members) {
        this.members = List.copyOf(members); // copy!
    }
}

2. Return a protected version (getter)

public List<String> getMembers() {
    return members; // List.copyOf already returned an immutable list
}

3. Don’t store direct references

// BAD
public Group(List<String> members) {
    this.members = members; // direct reference — can be modified externally
}

Middle Level

Protection on creation

// Java 10+ — best option
this.list = List.copyOf(input);

// Java 8
this.list = Collections.unmodifiableList(new ArrayList<>(input));

Protection on read

  • If the field is already immutable (List.copyOf) — can return directly
  • If mutable — wrap: Collections.unmodifiableList(this.list)

The deep immutability problem

List.copyOf protects the container, but not the elements:

public record Team(String name, List<User> users) {
    public Team {
        users = List.copyOf(users); // container is protected
    }
}
// But: team.users().get(0).setName("...") — POSSIBLE!

Solution — deep copying:

public record Team(String name, List<User> users) {
    public Team {
        users = users.stream()
            .map(u -> new User(u.getName(), u.getRole())) // copy each
            .toList();
    }
}

Deep copying is only necessary if the collection elements are mutable. For String, Integer, Record — List.copyOf() is sufficient.

Specialized libraries

  • Vavr — persistent collections (structural sharing)
  • GuavaImmutableList, ImmutableMap

Senior Level

Distinguish Unmodifiable and Immutable

Characteristic Unmodifiable Wrapper Immutable Copy
Data copy No Yes
Link to original Live Broken
Creation speed O(1) O(n)
Safety Partial Full
List<String> original = new ArrayList<>(List.of("A"));
List<String> unmod = Collections.unmodifiableList(original);
original.set(0, "B"); // unmod also changed! — live reference

List<String> immutable = List.copyOf(original);
original.set(0, "C"); // immutable stayed "B" — independent copy

Copying performance

  • List.copyOf() is optimized: if the input is already immutable, returns the same object — saves memory and CPU.
  • For large collections in hot paths, consider Vavr with $O(\log n)$ via structural sharing
  • Deep copying — $O(n \times m)$, where m = graph depth

Records and collections

public record UserGroup(String name, List<String> members) {
    public UserGroup {
        members = List.copyOf(members); // mandatory in compact constructor
    }
}

Summary for Senior

  • Always break the link with the original collection via copying
  • Distinguish “unmodifiable wrapper” from “immutable copy”
  • JDK collections — only Shallow Immutability
  • For large data — persistent data structures (Vavr)
  • Deep Copy is mandatory for mutable collection elements

Interview Cheat Sheet

Must know:

  • Three rules: copy in constructor, return protected version from getter, don’t store direct references
  • List.copyOf(input) — best option (Java 10+), creates an independent immutable copy
  • Deep immutability: if elements are mutable, need to clone each via stream.map
  • Unmodifiable vs Immutable: wrapper (O(1), linked) vs copy (O(n), independent)
  • Records: must copy collections in compact constructor
  • List.copyOf is optimized: if input is already immutable, returns the same object

Frequent follow-up questions:

  • Does List.copyOf protect elements? — No, only the container; mutable elements can still be modified
  • When is deep copying NOT needed? — When elements are themselves immutable (String, Integer, Record)
  • UnmodifiableList vs List.copyOf in getter? — If field is already List.copyOf — return directly; if mutable — unmodifiableList
  • Vavr vs JDK collections? — Vavr: structural sharing O(log n), JDK: full copying O(n)

Red flags (do NOT say):

  • “List.copyOf = full protection” — it’s shallow, elements are the same
  • “Can return the original if the collection is private” — caller may have a reference to the original
  • “Deep copying is always needed” — excessive for String/Integer elements
  • “UnmodifiableList in constructor — protection” — it’s a wrapper, not a copy

Related topics:

  • [[9. What to do if class field references a mutable object]]
  • [[10. What is defensive copy]]
  • [[12. How to protect a collection from modification]]
  • [[14. What is the difference between shallow copy and deep copy]]
  • [[20. What is Record and how does it help create immutable classes]]