snaran
snaran

Reputation: 175

Template parameter difference between Collectors.toList and Stream.toList

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

Answers (2)

Brian Goetz
Brian Goetz

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

Eugene
Eugene

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

Related Questions