Java: Stream.toList() and Generics
Observing a limitation of Java’s Stream.toList() with regard to generics.
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()”.