Press "Enter" to skip to content

Nulls Against Collectors

Converting a Java Stream into a List sounds trivial but can be surprising sometimes, especially when dealing with nulls and immutability.

Converting Stream<T> to List<T>

This is one of the most basic and mocked ways of converting Streams to Lists – I’m sure you know what I’m talking about, and I’m sure you’ve done this hundreds of times already:

stream().map(...).collect(Collectors.toList());

You might ask… what’s wrong with it? And the answer is: absolutely nothing.

However, you need to accept the fact that the result List is mutable(not enforced by any contract), supports null values, and that the syntax is overly verbose:

Luckily, in JDK10 and JDK16, we got alternatives that deal with the mentioned issues… and also brought some confusion.

Converting Stream<T> to an unmodifiable List<T> (JDK10)

Since JDK10, converting a Stream instance into an unmodifiable List is as easy as applying a dedicated Collector:

List<Integer> l = Stream.of(1).collect(Collectors.toUnmodifiableList());

However, this is not as easy as replacing every Collectors.toList() call with Collectors.toUnmodifiableList(). It turns out that the Collector returns a List that does not support null values!

And we learn it the hard way:

Stream.of(1, 2, null).collect(Collectors.toUnmodifiableList()).add(2);

// java.lang.NullPointerException 
// at java.base/java.util.Objects.requireNonNull(Objects.java:208)

If you want to have an unmodifiable List with null values support, you need to fall back to the JDK8 unmodifiable List trick:

List<Integer> l = Stream.of(1, 2, null)
  .collect(Collectors.collectingAndThen(
    Collectors.toList(), 
    Collections::unmodifiableList));

However, if you’re on a JDK16+, there’s a better way.

Converting Stream<T> to an unmodifiable List<T> (JDK16)

Luckily, JDK16 brought in the most convenient of all options: Stream#toList.

This is not a Collector but a convenience method callable right on a Stream instance:

List<Integer> l = Stream.of(1, 2, null).toList();

The resulting List is unmodifiable and supports null values (however, this is not explicitly enforced by the contract).

But, I would be cautious when blindly replacing Collectors.toList() and Collectors.toUnmodifiableList() with it because of two reasons:

  • Stream#toList returns an unmodifiable List instance, so if the List instance was passed around and modified, this would now result in an UnsupportedOperationException. Ouch!
  • Stream#toList returns a List instance that supports null values

Unfortunately, IntelliJ IDEA will suggest converting from one to the other right away, which can backfire at times:

Summary

Two new additions to Stream API provide convenient ways of converting a Stream instance into a List.

However, those should be used carefully since they are not drop-in replacements due to slightly different semantics.




If you enjoyed the content, consider supporting the site: