Files

immutable

Companion code for the ankurm.com post "Immutable Collections in Java: List.of vs unmodifiableList vs copyOf." Module in java-core-examples, the Java-core series.

Versions this was built and tested against

Component Version Notes
JDK 25.0.4.1+1 (Temurin, LTS) List.of/Map.of/Set.of shipped in Java 9 (2017); List.copyOf/Map.copyOf/Set.copyOf in Java 10 (2018). Both unchanged since.
Guava 33.7.2-jre Newest GA per Maven Central at the time of writing.
JMH 1.37
JUnit Jupiter 5.11.0
Maven 3.9.11

Quickstart

export JAVA_HOME=/path/to/jdk-17-or-newer
mvn compile
java -cp "target/classes:$(find ~/.m2 -name 'guava-*.jar' ! -name '*sources*' | head -1)" \
  com.ankurm.immutable.NullHandlingDemo

scripts/run-all.sh regenerates every file in output/, including a fresh JMH run. scripts/run.sh <ClassName> runs one demo ad hoc.

What's in here

File What it shows
NullHandlingDemo.java List.of/List.copyOf reject null elements at construction; Arrays.asList and Collections.unmodifiableList do not check for null themselves - they only block structural writes.
ViewsVsCopiesDemo.java Collections.unmodifiableList is a live view over its backing collection; List.copyOf/Map.copyOf take an independent snapshot. Also pins List.copyOf's documented no-op optimisation when the source is already immutable.
GuavaComparisonDemo.java Where Guava's ImmutableList still differs from List.of/List.copyOf today: mainly the builder API, since null-rejection and copy-vs-view behaviour now match.
DefensiveCopyRecordDemo.java A record's compact constructor does not defensively copy a List field for you - LeakyOrder shows the leak both at construction and through the accessor; SafeOrder fixes both with List.copyOf(items).
ImmutableOverheadBenchmark.java JMH: construction cost of wrap-vs-copy at two sizes, and read-by-index cost across a plain ArrayList, an unmodifiable wrapper, and an immutable copy.
ImmutableCollectionsTest.java Pins all of the above as assertions, 12/12 passing.
output/01-06 Captured runs of the four demos, the raw JMH report, and the test suite.

Notes worth knowing before reading the post

  • Collections.unmodifiableList has no null-check of its own. It wraps whatever list you hand it and blocks its own mutator methods; whether null is already in there, or gets added later through a reference you kept to the backing list, is entirely up to that backing list. See output/01.
  • Construction cost and read cost tell opposite stories. scripts/run-all.sh's JMH run (output/05) shows wrapping is O(1) regardless of size while copyOf is O(n) and gets measurably slower as the source grows - but reading through any of the three (plain list, unmodifiable wrapper, immutable copy) comes out statistically indistinguishable, because the extra delegation call in the wrapper is cheap enough for the JIT to not matter at this scale. Read the ratios, not the absolute ops/ms - this is a shared, multi-tenant sandbox.
  • A record field typed as List<String> is not immutable just because the record is. DefensiveCopyRecordDemo is the one demo in this module that most people get wrong in real code - see output/04.

License

MIT - see the repo-wide LICENSE.