Reputation: 175
Found a difference between collect(Collectors.toList()) and Stream.toList(). See
class Animal { }
class Cat extends Animal { }
record House(Cat cat) { }
class Stuff {
public static void function() {
List<House> houses = new ArrayList<>();
List<Animal> animals1 =
houses.stream()
.map(House::cat)
.collect(Collectors.toList()); // ok
List<Animal> animals2 =
houses.stream()
.map(House::cat).toList(); // compile error
List<Animal> animals3 =
houses.stream()
.map(House::cat)
.map(cat -> (Animal) cat).toList(); // ok
}
}
The collect(Collectors.toList()) is able to give me a List of Animal or List of Cat. But the Stream.toList() can only give a List of Cat.
The question is there any way to make Stream.toList() work. In my real world example I have a class which overrides shutdownNow, which returns a list of Runnable, so my class was calling something.stream().collect(Collectors.toList()), but something.stream().toList() returns a list of MyRunnable.
A part of me wishes they declared the function as default <U super T> List<U> toList()
instead of default List<T> toList()
, though strangely that is a compile error on my machine (my compiler seems to be ok with U extends T, not U super T).
Upvotes: 2
Views: 1273
Reputation: 95346
There is a simple answer here.
Start with
var stream = houses.stream().map(House::cat)
Here, stream
has type Stream<Cat>
. The Stream::toList
method gives you a list of the stream element type, which here is Cat
. So stream.toList()
is of type List<Cat>
. There's no choice here.
Collector
has multiple type variables, including the type of the input element, and the type of the output result. There is a lot of flexibility to create a Collector
that takes in Cat
and produces List<Cat>
, List<Animal>
, Set<Animal>
, etc. This flexibility is partially hidden by the genericity of Stream::collect
(and also the generic method Collectors::toList
); inferring the generic type parameters for this method can take into account the desired result type on the LHS. So the language papers over the gap between Cat
and Animal
for you, because there's another level of indirection between the stream and the result.
As @Eugene pointed out, you could get a more general type out:
List<? extends Animal> animals2 = houses.stream().map(House::cat).toList()
This has nothing to do with streams; it is just because List<? extends Animal>
is a supertype of List<Cat>
. But there's an easier way. If you want a List<Animal>
, and you want to use toList()
, change the stream type to a Stream<Animal>
:
List<Animal> animals = houses.stream().map(h -> (Animal) h.cat()).toList()
Stream::map
is also generic, so by having the RHS of the lambda be Animal
, not Cat
, you get a Stream<Animal>
out, and then toList()
gives you a List<Animal>
.
You could also break it up, if you like that better:
List<Animal> animals = houses.stream().map(House::cat)
.map(c -> (Animal) c).toList()
What is tripping you up is that, because collect
is a generic method (and so Collectors::toList
), there is additional flexibility to infer a slightly different type, whereas in the simpler stream, everything is more explicit, so if you want to adjust the types, you have to do that in imperative code.
Upvotes: 7
Reputation: 120848
That is impossible to achieve.
Collectors::toList
has a declaration of ? extends T
. On the other hand Stream::toList
returns a List<T>
, you are stuck.
You can workaround that (partially), via:
List<? extends Animal> animals2 = houses.stream().map(House::cat).toList();
What you are thinking is U super T
, but that is not supported, unfortunately. People occasionally found good real use cases for it - but support is not there.
Upvotes: 3