Are collections from List.of / Map.of deeply immutable, and what does that mean for stored mutable objects and thread safety?
answer
- Structure locked, contents' internals not
- Shallow != deep immutability
- Mutable element inside stays mutable
- Deep immutability = compose immutable pieces
- Immutable + immutable elements = thread-safe, no locks
basics
~20 sThey are only shallowly immutable. You cannot add, remove, or replace elements, but if an element is itself a mutable object (like an ArrayList or a bean), you can still change that object's internal state. The collection structure is fixed; the contents' internals are not protected.
solid answer
~50 sList.of and friends guarantee structural immutability: the set of references the collection holds can never change, and mutating operations throw. But this is shallow, not deep. If you store a reference to a mutable object, that object can still be mutated through any reference to it, including the one inside the collection. For example, List.of(new ArrayList<>(...)) prevents replacing the inner list but does nothing to stop someone calling add() on that inner list. For genuine deep immutability you must store only immutable elements (Strings, boxed primitives, records of immutable fields, other immutable collections). On thread safety: a structurally immutable collection of truly immutable elements is fully thread-safe to share with no synchronization, because nothing can change. But if the elements are mutable, you reintroduce the usual concurrency hazards on those elements, even though the collection itself is fixed.
code
java · 9 linesList<List<Integer>> outer = List.of(new ArrayList<>(List.of(1, 2)));
// outer.add(List.of(9)); -> UnsupportedOperationException (structure locked)
outer.get(0).add(3); // allowed: inner ArrayList is mutable
System.out.println(outer); // [[1, 2, 3]]
// Deep immutability: compose immutable pieces
List<List<Integer>> deep = List.of(List.of(1, 2));
// deep.get(0).add(3); -> UnsupportedOperationExceptiongo deeper
Knows the collection itself can't be added to or modified.
Understands shallow vs deep: that mutable elements inside can still change.
Explains how to achieve deep immutability by composition and the thread-safety consequences of mutable vs immutable elements.
Reasons about safe publication, designs immutable domain types, and sets conventions to prevent leaking mutable state through 'immutable' APIs.
## What 'immutable' really covers When we say `List.of(...)` is *immutable*, we mean the collection's *structure* is fixed: it always holds the same references, in the same positions, and any attempt to change that (`add`, `set`, `remove`, `clear`) throws `UnsupportedOperationException`. This is called **shallow** or **structural** immutability. ## Shallow vs deep - *Shallow immutability*: the container cannot change which objects it points to. - *Deep immutability*: neither the container nor any object it (transitively) contains can change. A reference is just an address pointing at an object. A `List<StringBuilder>` from `List.of(...)` permanently holds the same `StringBuilder` references — but a `StringBuilder` is itself mutable, so its *contents* can change even though the list's pointer to it cannot. ```java StringBuilder sb = new StringBuilder("hi"); List<StringBuilder> list = List.of(sb); // list.set(0, ...) -> UnsupportedOperationException (structure is locked) sb.append(" there"); // allowed! the element's own state changed System.out.println(list.get(0)); // "hi there" ``` The list never changed which object it points to, yet the observable contents changed. That is shallow immutability. ## A nested-collection example ```java List<List<Integer>> outer = List.of(new ArrayList<>(List.of(1, 2))); // outer.add(...) throws outer.get(0).add(3); // the INNER ArrayList is mutable System.out.println(outer); // [[1, 2, 3]] ``` The outer list is immutable; the inner one is not, so the whole structure effectively changed. ## How to get deep immutability There is no flag for it — you must compose immutable pieces: 1. Store only inherently immutable elements: `String`, boxed primitives (`Integer`, `Long`…), `enum`, `java.time` types, `record`s whose fields are all immutable. 2. For nested collections, store other immutable collections (`List.of(List.of(1, 2))`). 3. For your own classes, make them immutable (final fields, no setters, defensive copies of any mutable inputs, and don't leak internal references). ## Thread-safety implications This is where it matters most. *Immutable objects are inherently thread-safe*: if nothing about an object can change after construction, multiple threads can read it concurrently with no locks, no `volatile`, no data races. - A `List.of` of truly immutable elements → safe to share freely across threads. - A `List.of` of *mutable* elements → the **collection** is safe (no thread can restructure it), but the **elements** are not: two threads mutating a shared inner `ArrayList` still race and need synchronization. The structural immutability gives a false sense of safety here. There is also a subtle publication benefit: the JDK's immutable collections are *safely published* — once another thread sees the reference, it sees a fully constructed, consistent collection — which is not guaranteed for a half-built `ArrayList` shared via a data race. ## Takeaway `List.of`/`Set.of`/`Map.of` lock the container, not the contents. Treat them as a building block: combine them with immutable elements to reach deep immutability and unconditional thread safety; if you stuff mutable objects inside, you keep all the concurrency obligations on those objects.
- If I do List.of(new ArrayList<>()), can I still add to that inner ArrayList?Yes. The outer list locks its references, but the inner ArrayList is a mutable object you can still modify; immutability is shallow.
- Is a List.of of Strings safe to share across threads without synchronization?Yes. Strings are immutable and the collection is structurally immutable, so concurrent reads are safe with no locks.
saying these in an interview costs you the question
- Claiming List.of makes contained objects immutable too
- Assuming a List.of of mutable objects is automatically thread-safe to mutate
- Thinking nested ArrayList inside List.of cannot be changed
- Believing there's a 'deep immutable' flag on the factories