🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:220)
at java.base/java.util.ImmutableCollections$Map1.<init>(ImmutableCollections.java:1350)
at java.base/java.util.Map.of(Map.java:1364)⚡ Quick Fix Works 80% of the time
Filter nulls before freezing, or fall back to HashMap + unmodifiable wrapper.
Map<String, Config> safe = configs.entrySet().stream()
.filter(e -> e.getValue() != null)
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));🧠 Why this Happens
Tap to expand the deep technical explanation
Immutable factories validate eagerly: ImmutableCollections constructors call Objects.requireNonNull per key and value during construction, so the NPE stack names the FACTORY line rather than some distant consumer. Design rationale: collections that cannot hold nulls make later reads total functions, whereas late-failing NPEs from HashMap look identical to genuine logic bugs and surface far from the injection point.
The HITEC City Parking Spot Analogy:
Airport security checks every bag BEFORE boarding, not mid-flight: the offender is identified at the door, not discovered over the ocean.
🔁 How to Reproduce Confirm this is your error
Map<String, String> ok = Map.of("a", null); // NPE - null value Map<String, String> also = Map.of(null, "v"); // NPE - null key List<String> l = Arrays.asList("x", null); // fine (mutable view) List<String> m = List.of("x", null); // NPE
🛠️ Solutions (5 Ways to Fix)
Filter nulls before the factory call
👉 Use this if source data legitimately contains absent entries.
Stream filtering (or entrySet removeIf on a temp map) drops null-bearing entries so the immutable factory receives clean pairs.
Map<K, V> frozen = raw.entrySet().stream()
.filter(e -> e.getKey() != null && e.getValue() != null)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));Allow nulls? Use HashMap + Collections.unmodifiableMap
👉 Use this if the domain truly requires storing null values.
Build a mutable HashMap (accepts null keys/values), then wrap unmodifiable for safety; you trade eager rejection for legacy semantics.
Map<String, String> m =
Collections.unmodifiableMap(new HashMap<>(raw));Model absence as Optional<V> instead of null values
👉 Use this if designing new APIs around immutable maps.
Optional-valued maps express missing entries in the type system, eliminating both null values and the factory rejection problem.
Map<String, Optional<Config>> registry = ...;
registry.get(key).orElseGet(Config::defaults);Null Object sentinel for legacy consumers
👉 Use this if downstream code cannot handle Optionals.
A shared EMPTY_CONFIG instance stands in for absent values, keeping immutable factories happy while preserving non-null reads.
Config cfg = found != null ? found : Config.EMPTY;
var pinned = Map.of("cfg", cfg);Validate with named-field messages before freezing
👉 Use this if construction failures need actionable logs.
Manual requireNonNull(x, "config.timeout") checks ahead of Map.of produce errors naming the field instead of a bare ImmutableCollections frame.
Objects.requireNonNull(timeoutMs, "timeoutMs must be set");
return Map.of("timeout", timeoutMs);📋 Version Notes
No of() factories; HashMap accepted null keys/values silently.
Factories introduced WITH eager null rejection; bare NPE from ImmutableCollections frames.
Map.copyOf/Set.copyOf/List.copyOf enforce identical rules.
🛡️ How to Prevent This Next Time
Decide the null policy per collection at design time: immutable factories mean NO nulls anywhere - sanitize inputs upstream.