|
| 1 | +ImmutableMap — Internal Working |
| 2 | + |
| 3 | +Java provides multiple ways to create "immutable" or unmodifiable maps. There are two common patterns: |
| 4 | + 1) an immutable *view* over a mutable map (`Collections.unmodifiableMap(...)`), |
| 5 | + 2) truly immutable maps created by factory methods (`Map.of(...)` and `Map.ofEntries(...)`). |
| 6 | +Each has different internal characteristics and use-cases. |
| 7 | + |
| 8 | +1. Collections.unmodifiableMap(map) |
| 9 | +---------------------------------- |
| 10 | +- What it is: |
| 11 | + Creates an *immutable view* (a read-only wrapper) of an existing Map instance. |
| 12 | + |
| 13 | +- Internal working: |
| 14 | + - `Collections.unmodifiableMap(m)` returns a wrapper object (an instance of a private static class |
| 15 | + inside `Collections`) that implements `Map`. |
| 16 | + - The wrapper delegates all read operations (get, containsKey, entrySet, etc.) directly to the underlying map instance. |
| 17 | + - Mutating calls (put, remove, clear, putAll, etc.) on the wrapper throw `UnsupportedOperationException`. |
| 18 | + - The wrapper is a *shallow* wrapper — it does not copy entries. It only prevents modifications via the wrapper reference. |
| 19 | + |
| 20 | +- Important consequences: |
| 21 | + - If the original map (`map1`) is modified directly (not via the wrapper), those changes are visible through the wrapper. |
| 22 | + - The wrapper is lightweight and cheap because it only stores a reference to the original map. |
| 23 | + - Not a true immutable snapshot — use only for defensive read-only sharing when you control who can modify the original. |
| 24 | + |
| 25 | +- Use-cases: |
| 26 | + - Provide read-only access to an internal data structure without copying. |
| 27 | + - Protect a map from modification by API consumers while still allowing internal updates. |
| 28 | + |
| 29 | +- ASCII (conceptual) |
| 30 | + [map1 (modifiable)] <---- wrapper delegates reads ---- [unmodifiableMap (view)] |
| 31 | + put("A",1) --> map1 changes visible via unmodifiableMap.get("A") |
| 32 | + |
| 33 | +2. Map.of() |
| 34 | +----------- |
| 35 | +- What it is: |
| 36 | + - Factory methods introduced in Java 9 to create truly immutable small maps. |
| 37 | + - Example: `Map.of("A", 1, "B", 2)`. |
| 38 | + |
| 39 | +- Internal working: |
| 40 | + - `Map.of(...)` implementations create an internal compact, immutable map representation (not a wrapper). |
| 41 | + - The returned instance contains its own internal arrays/structures for keys and values. |
| 42 | + - It is not backed by any external map; it cannot be changed after creation. |
| 43 | + - Typically optimized for small fixed sizes (special-cased implementations for 0..10 entries). |
| 44 | + |
| 45 | +- Important consequences: |
| 46 | + - Truly immutable — no external modification possible and no view semantics. |
| 47 | + - Attempting `put` or `remove` throws `UnsupportedOperationException`. |
| 48 | + - Very memory- and CPU-efficient for small constant maps. |
| 49 | + - Limited convenience overloads: `Map.of` has overloaded variants up to 10 key-value pairs. |
| 50 | + |
| 51 | +- Use-cases: |
| 52 | + - Small fixed configuration maps or constants defined at startup. |
| 53 | + - When you want a true immutable map with no risk of external mutation. |
| 54 | + |
| 55 | +- ASCII (conceptual) |
| 56 | + [Map.of instance] -> independent immutable storage of {A=1, B=2} |
| 57 | + Changes to other maps do not affect it. |
| 58 | + |
| 59 | +3. Map.ofEntries() |
| 60 | +------------------ |
| 61 | +- What it is: |
| 62 | + - Also Java 9; flexible factory method that accepts many `Map.entry(...)` items and returns a truly immutable map. |
| 63 | + - Not limited to 10 entries. |
| 64 | + |
| 65 | +- Internal working: |
| 66 | + - Builds an independent immutable internal structure from the provided entries. |
| 67 | + - Likely uses an optimized internal representation to store the entries compactly (array-based or similar). |
| 68 | + - Not a wrapper; it is a separate object with no references to any source map. |
| 69 | + |
| 70 | +- Important consequences: |
| 71 | + - Same immutability guarantees as `Map.of`. |
| 72 | + - Useful when you need more than 10 entries or want to construct an immutable map programmatically. |
| 73 | + |
| 74 | +- Use-cases: |
| 75 | + - Large constant maps at initialization, configuration mapping, read-only lookup tables. |
| 76 | + |
| 77 | +Comparison summary (quick) |
| 78 | +-------------------------- |
| 79 | +- Backing: |
| 80 | + - `Collections.unmodifiableMap`: wrapper around existing map (delegates reads). |
| 81 | + - `Map.of` / `Map.ofEntries`: independent immutable instances (no backing map). |
| 82 | + |
| 83 | +- Mutability: |
| 84 | + - `unmodifiableMap`: prevents modification via wrapper but underlying map can still change. |
| 85 | + - `Map.of` / `Map.ofEntries`: fully immutable (cannot change at all). |
| 86 | + |
| 87 | +- Memory & performance: |
| 88 | + - `unmodifiableMap`: minimal overhead, cheap to create. |
| 89 | + - `Map.of` / `Map.ofEntries`: optimized internal structures, efficient for lookups, slightly higher creation cost but immutable. |
| 90 | + |
| 91 | +Practical examples and notes |
| 92 | +---------------------------- |
| 93 | +- When to choose which: |
| 94 | + - Want to expose internal map read-only while still updating it internally? Use `Collections.unmodifiableMap`. |
| 95 | + - Need a true constant map that never changes and is safe to share across threads without synchronization? Use `Map.of` or `Map.ofEntries`. |
| 96 | + - Need >10 entries and immutability? Use `Map.ofEntries`. |
| 97 | + |
| 98 | +- Thread-safety: |
| 99 | + - `Map.of` / `Map.ofEntries` are inherently thread-safe (immutable). |
| 100 | + - `Collections.unmodifiableMap` is thread-safe for reads only if the underlying map is thread-safe; otherwise concurrent modifications of the backing map can lead to race conditions. |
| 101 | + |
| 102 | +- Example patterns: |
| 103 | + - Defensive API: |
| 104 | + `public Map<K,V> getSettings() { return Collections.unmodifiableMap(this.settings); }` |
| 105 | + (internal code can still update `settings`.) |
| 106 | + |
| 107 | + - Constant config: |
| 108 | + `private static final Map<String,Integer> LIMITS = Map.of("A", 1, "B", 2);` |
| 109 | + |
| 110 | +ASCII diagram: wrapper vs independent |
| 111 | +------------------------------------ |
| 112 | + Wrapper (unmodifiableMap): |
| 113 | + +-----------------+ +--------------------+ |
| 114 | + | unmodifiableMap | --delegates--> | backing HashMap | |
| 115 | + | (throws on put) | | {A=1, B=2} | |
| 116 | + +-----------------+ +--------------------+ |
| 117 | + |
| 118 | + Independent immutable (Map.of / Map.ofEntries): |
| 119 | + +-------------------------+ |
| 120 | + | immutableMap (Map.of) | |
| 121 | + | internal storage {A=1} | |
| 122 | + +-------------------------+ |
| 123 | + (no link to any other map, cannot change) |
| 124 | + |
| 125 | +Final takeaways |
| 126 | +--------------- |
| 127 | +- `Collections.unmodifiableMap` is a cheap defensive view; use when you must share a mutable map read-only. |
| 128 | +- `Map.of` and `Map.ofEntries` produce true immutable maps suitable for constants and thread-safe shared data. |
| 129 | +- Choose the one that matches whether you need a view-on-existing-data or a separate immutable snapshot. |
0 commit comments