Observing a limitation of Java’s Stream.toList() with regard to generics.

Photo by Ana Municio, https://unsplash.com/photos/PbzntH58GLQ

Before Java 16, we were collecting stream elements into lists with the following code:

// turn this into a List<Integer>:
Stream<Integer> stream = List.of(1, 2).stream();

List<Integer> l = stream.collect(Collectors.toList());

With a static import, we could save a few keystrokes:

import static java.util.stream.Collectors.toList;
...
List<Integer> l = stream.collect(toList());

Now we can save even more keystrokes (and the static import) by using the “Stream.toList” method:

List<Integer> l = stream.toList();

Note that in this case the returned list is not modifiable any longer.

Suppose that for some reason we don’t want to have a list of integers as a result, but something less specific. This works fine with the old collector method:

List<? extends Number> l = stream.collect(toList());
List<Number> m = stream.collect(toList());

Assignment to the upper-bounded wildcard list also works with the new method:

List<? extends Number> l = stream.toList();

But the assignment to a list of a less specific type fails:

// "Type mismatch: cannot convert from List<Integer> to List<Number>"
List<? extends Number> l = stream.toList();

The reason is that “Stream.toList()” always returns a List<Integer> when invoked on a Stream<Integer>. And as we know from my previous article about “Understanding Java Generics”, this is forbidden because it would allow us to illegally smuggle E-Bikes into a list of Tandems. The old collector is smarter in that it infers its return type from the left-hand side of the assignment.

You might never run into this particular problem, but if you do, you can just fall back to using “Collectors.toList()” instead of “Stream.toList()”.