🔴 The Error You're Seeing

Confirm this matches your console output. If it does, you're in the right place.

ERROR LOGjava.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)

Solution 1✓ Most common cause

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));
Solution 2

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));
Solution 3

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);
Solution 4

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);
Solution 5

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

Java 8

No of() factories; HashMap accepted null keys/values silently.

Java 9+

Factories introduced WITH eager null rejection; bare NPE from ImmutableCollections frames.

Java 10+

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.