SSerguey Asael Shinder
Java coding notes: the JVM, and writing software that lasts

Serguey Asael Shinder: List.of(...).contains(null) throws, and the javadoc allows it

· by Serguey Asael Shinder / Serguey Shinder

A check like this looks harmless:

List<String> allowed = List.of("read", "write");
if (allowed.contains(role)) { ... }

If role is null, it does not return false. It throws NullPointerException.

Serguey Asael Shinder: List.of(...).contains(null) throws, and the javadoc allows it
List.of(...).contains(null) throws, and the javadoc allows it — Serguey Asael Shinder

That surprises people because a HashSet or an ArrayList would simply answer false. But the contract was never "contains answers false for null". The javadoc of Collection.contains) lists, under its exceptions: NullPointerException — if the specified element is null and this collection does not permit null elements (optional). Optional means an implementation may throw or may not.

The collections from the static factories are the ones that do not permit nulls. The List javadoc, section "Unmodifiable Lists" says the lists created by List.of and List.copyOf "disallow null elements". And the JDK's implementation chooses the throwing option: in ImmutableCollections.java the small two-element list starts indexOf with Objects.requireNonNull(o), the general list throws NullPointerException when nulls are not allowed and the argument is null, and contains is implemented through indexOf. The set built by Set.of calls Objects.requireNonNull(o) in contains directly, and the single-entry Map.of relies on o.equals(k0) in containsKey — the source comment calls it an "implicit nullcheck".

What to do about it:

The rule underneath is old: an interface method whose javadoc says "optional" next to an exception is telling you that two correct implementations may behave differently on that input. The factory collections are simply the ones most people meet first.