immutable: List.of vs unmodifiableList vs copyOf - null handling, views vs copies, JMH wrap/copy overhead, a Guava comparison, and the record defensive-copy trap
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
=== List.of(...) - rejects null at construction time ===
|
||||
List.of("a", null, "c") threw NullPointerException
|
||||
|
||||
=== Arrays.asList(...) - allows null, it's just a view over the array ===
|
||||
Arrays.asList("a", null, "c") = [a, null, c]
|
||||
|
||||
=== Collections.unmodifiableList(...) - allows whatever the backing list allows ===
|
||||
unmodifiableList over a list already containing null = [a, null, c]
|
||||
(it has no null-check of its own - it only blocks structural writes, see below)
|
||||
|
||||
=== List.copyOf(...) - rejects null, same as List.of ===
|
||||
List.copyOf(aListContainingNull) threw NullPointerException
|
||||
-> List.copyOf() re-validates elements, it does not just wrap and trust the source
|
||||
|
||||
=== Collections.unmodifiableList is a VIEW, not a copy: mutating the backing list shows through ===
|
||||
before: unmodView = [a, null, c]
|
||||
after backing.set(0,...) and backing.add("d"): unmodView = [A-CHANGED, null, c, d]
|
||||
-> the "unmodifiable" promise is about the VIEW's own mutator methods, not about the data being frozen
|
||||
|
||||
=== unmodifiableList's own mutators still throw, even though the backing list is mutable ===
|
||||
unmodView.add("e") threw UnsupportedOperationException
|
||||
@@ -0,0 +1,21 @@
|
||||
=== unmodifiableList is a VIEW: changes to the backing list show through ===
|
||||
backing = [a, b, c]
|
||||
view = [a, b, c]
|
||||
after backing.add("d"):
|
||||
backing = [a, b, c, d]
|
||||
view = [a, b, c, d] <- changed, with no code touching 'view' directly
|
||||
|
||||
=== List.copyOf is a SNAPSHOT: changes to the source do NOT show through ===
|
||||
source = [x, y, z]
|
||||
copy = [x, y, z]
|
||||
after source.add("w"):
|
||||
source = [x, y, z, w]
|
||||
copy = [x, y, z] <- unchanged, it was a real copy at the moment copyOf() ran
|
||||
|
||||
=== List.copyOf has a documented optimization: copying an already-immutable list is a no-op ===
|
||||
List.copyOf(List.of(...)) returns the SAME instance: true
|
||||
List.copyOf(new ArrayList<>(...)) returns a DIFFERENT instance: true
|
||||
|
||||
=== Map and Set follow the same pattern as List ===
|
||||
unmodMapView after mutating backing map = {one=1, two=2} (view: changed)
|
||||
mapCopy after mutating the ORIGINAL map = {one=1} (copy: unchanged)
|
||||
@@ -0,0 +1,19 @@
|
||||
=== Both reject null, same as List.of ===
|
||||
ImmutableList.of("a", null, "c") threw NullPointerException - same as List.of
|
||||
|
||||
=== Guava's builder tolerates a size that grows past what you declared; List.of has no builder ===
|
||||
ImmutableList.builder() result = [1, 2, 3, 4, 5]
|
||||
-> java.util has no equivalent builder; you either know all elements up front for List.of(...)
|
||||
or you build an ArrayList and call List.copyOf(...) at the end - this demo's RecordDemo does exactly that.
|
||||
|
||||
=== Element limit: List.of has none in practice; the old Arrays.asList(E...) varargs ceiling is long gone ===
|
||||
List.of(... 5000 elements ...).size() = 5000 (List.of(E...) switched to a varargs array long ago - the old 1-10 overloads are just JIT-friendlier for small sizes)
|
||||
|
||||
=== Guava's ImmutableList also exposes a reverse() view and asList() - conceptually close to JEP 431's reversed() ===
|
||||
built.reverse() = [5, 4, 3, 2, 1] (Guava's reverse() predates java.util's reversed() by over a decade)
|
||||
|
||||
=== Where they genuinely differ: Guava's collections are NOT in java.base, so they ship as a real dependency ===
|
||||
Adding Guava to a project to get ImmutableList today mostly buys you nothing List.of/List.copyOf
|
||||
doesn't already give you for the collection types themselves - the builder pattern above is the
|
||||
real remaining reason, plus ImmutableList/ImmutableMap/ImmutableSet having first-class equivalents
|
||||
for ImmutableMultimap, ImmutableTable and other Guava-only collection types the JDK has no analog for.
|
||||
@@ -0,0 +1,18 @@
|
||||
=== LeakyOrder: a record field that LOOKS immutable but isn't ===
|
||||
leaky.items() right after construction = [widget, gadget]
|
||||
caller mutates their OWN list reference afterwards: callerList.add(...)
|
||||
leaky.items() now = [widget, gadget, SNEAKY EXTRA ITEM] <- the record's field changed too, because it's the SAME list object
|
||||
|
||||
=== LeakyOrder.items() also returns the live mutable reference - callers can mutate it directly ===
|
||||
leaky.items() after leaky.items().add(...) = [widget, gadget, SNEAKY EXTRA ITEM, MUTATED THROUGH THE ACCESSOR]
|
||||
|
||||
=== SafeOrder: List.copyOf(...) in a compact constructor closes both holes ===
|
||||
safe.items() right after construction = [widget, gadget]
|
||||
caller mutates their OWN list reference afterwards: callerList2.add(...)
|
||||
safe.items() now = [widget, gadget] <- unchanged, it's an independent copy
|
||||
|
||||
=== And the accessor's result rejects mutation too, since List.copyOf returns an immutable list ===
|
||||
safe.items().add(...) threw UnsupportedOperationException
|
||||
|
||||
=== The constructor itself still rejects null elements, same as List.of ===
|
||||
new SafeOrder(..., listContainingNull) threw NullPointerException from inside List.copyOf(...)
|
||||
@@ -0,0 +1,10 @@
|
||||
Benchmark (size) Mode Cnt Score Error Units
|
||||
ImmutableOverheadBenchmark.construct_guavaImmutableListCopyOf 10 thrpt 6 16108.571 ± 1054.221 ops/ms
|
||||
ImmutableOverheadBenchmark.construct_guavaImmutableListCopyOf 1000 thrpt 6 1532.302 ± 53.937 ops/ms
|
||||
ImmutableOverheadBenchmark.construct_listCopyOf 10 thrpt 6 14442.559 ± 783.408 ops/ms
|
||||
ImmutableOverheadBenchmark.construct_listCopyOf 1000 thrpt 6 372.757 ± 63.463 ops/ms
|
||||
ImmutableOverheadBenchmark.construct_unmodifiableListWrap 10 thrpt 6 29294.827 ± 3954.701 ops/ms
|
||||
ImmutableOverheadBenchmark.construct_unmodifiableListWrap 1000 thrpt 6 21757.029 ± 2062.127 ops/ms
|
||||
ImmutableOverheadBenchmark.read_immutableCopy_get 1000 thrpt 6 2146.271 ± 121.977 ops/ms
|
||||
ImmutableOverheadBenchmark.read_plainArrayList_get 1000 thrpt 6 2141.758 ± 115.453 ops/ms
|
||||
ImmutableOverheadBenchmark.read_unmodifiableWrapper_get 1000 thrpt 6 2309.174 ± 116.629 ops/ms
|
||||
@@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.ankurm.immutable.ImmutableCollectionsTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.106 s -- in com.ankurm.immutable.ImmutableCollectionsTest
|
||||
Reference in New Issue
Block a user