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.unmodifiableListhas no null-check of its own. It wraps whatever list you hand it and blocks its own mutator methods; whethernullis already in there, or gets added later through a reference you kept to the backing list, is entirely up to that backing list. Seeoutput/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 whilecopyOfis 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.DefensiveCopyRecordDemois the one demo in this module that most people get wrong in real code - seeoutput/04.
License
MIT - see the repo-wide LICENSE.