Immutability: final, unmodifiable views, and defensive copies
Why final does not make an object immutable, why an unmodifiable view can still change, and how to write a value that truly cannot.
After this lesson you can
- Tell a final reference from an immutable object
- Explain the difference between Collections.unmodifiableList and List.copyOf
- Make a record with a collection component genuinely immutable
- Write "with" methods that return a changed copy instead of mutating
An immutable object is one whose observable state can never change
after construction. Immutable values are safe to share between threads
without locks, safe as HashMap keys, and safe to cache and hand out —
nobody can change them behind your back. String, Integer,
LocalDate and BigDecimal are all immutable.
final is about the variable, not the object
final List<String> names = new ArrayList<>(); means names can never
point at a different list. The list itself is as mutable as ever:
names.add("x") compiles and works. final on every field is necessary
for an immutable class, not sufficient.
A view is not a copy
Collections.unmodifiableList(list) returns a read-only view: you
cannot modify through it, but it reads straight through to the original
list, so anyone holding the original can still change what the view
shows. List.copyOf(list) (and List.of, Set.copyOf, Map.copyOf)
makes an unmodifiable copy that nothing can change. The copies also
reject null elements.
import java.util.*; public class Solution { public static List<String> describe() { List<String> source = new ArrayList<>(List.of("a", "b")); final List<String> view = Collections.unmodifiableList(source); final List<String> copy = List.copyOf(source); source.add("c"); // change the original after the fact List<String> out = new ArrayList<>(); out.add("view=" + view + " copy=" + copy); try { view.add("d"); } catch (UnsupportedOperationException e) { out.add("view.add: UnsupportedOperationException"); } try { List.copyOf(Arrays.asList("x", null)); } catch (NullPointerException e) { out.add("copyOf with a null: NullPointerException"); } return out; }}Records are only shallowly immutable
A record's fields are final, but a component of type List still
refers to whatever list the caller passed in. If the caller keeps that
list and changes it, the record changes too — and so does its
hashCode. The fix is a defensive copy in the compact constructor:
import java.util.*; public class Solution { record LeakyTeam(String name, List<String> members) {} record Team(String name, List<String> members) { Team { members = List.copyOf(members); // defensive copy } } public static List<String> describe() { List<String> people = new ArrayList<>(List.of("ana", "bo")); LeakyTeam leaky = new LeakyTeam("core", people); Team safe = new Team("core", people); int hashBefore = leaky.hashCode(); people.add("cy"); // the caller keeps editing return List.of( "leaky=" + leaky.members() + " hashChanged=" + (hashBefore != leaky.hashCode()), "safe=" + safe.members()); }}The recipe for an immutable class: make it final (or a record), make
every field private final, copy mutable inputs on the way in, never
hand out a mutable internal object on the way out (return the
unmodifiable copy, or a fresh copy), and provide no setters. To "change"
an immutable value, return a new one — withPrice(...),
plusDays(...), toUpperCase().
Common mistakes
- Assuming
finalfields make an object immutable; afinalarray or list field can still be filled and emptied. - Returning
Collections.unmodifiableList(internal)from a constructor argument the caller still owns — the caller can change it through the original reference, as the first example shows. - Calling
List.of(...)orList.copyOf(...)on data that may containnull; both throwNullPointerException. - "Updating" an immutable value and ignoring the result:
date.plusDays(1);on its own does nothing.
What interviewers ask
- How do you make a class immutable? Final class, private final fields, no setters, defensive copies of mutable inputs and outputs, and changes expressed as methods that return new instances.
- Is a record immutable? Shallowly: its fields cannot be reassigned, but mutable components (lists, arrays, dates from old APIs) need a copy in the compact constructor.
Exercise · Pro
Copy-on-write stock snapshots
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade