Why can't I use Stream#toList to collect a list of a class' interface in Java 16?

Viewed 2620

I'm streaming objects of a class implementing an interface. I'd like to collect them as a list of elements of the interface, rather than the implementing class.

This seems impossible with Java 16.0.1's Stream#toList method. For example in the code below, the last statement will fail to compile.

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class WhyDodo {

    private interface Dodo { }

    private static class FancyDodo implements Dodo { }

    public static void main(String[] args) {
        final List<Dodo> dodos = Stream.of(new FancyDodo()).collect(Collectors.toList());
        final List<FancyDodo> fancyDodos = Stream.of(new FancyDodo()).toList();

        final List<Dodo> noFancyDodos = Stream.of(new FancyDodo()).toList();
    }
}

We could explicitly cast each element from FancyDodo to Dodo. But at least for brevity, we could just as well use .collect(Collectors.toList()).

Why can't I use Stream#toList to collect a list of a class' interface in Java 16?

And if anyone has a better solution than explicitly casting, I'd be happy to hear as well :)

3 Answers

.collect(Collectors.toList()) works because the signature of collect is:

<R, A> R collect(Collector<? super T, A, R> collector);

the important part being ? super T

which means the toList() collector can be interpreted as Collector<Dodo,?,List<Dodo> (when you assign the result of .collect() to a List<Dodo>) even though the type of your stream is Stream<FancyDodo>.

On the other hand, the signature of Stream's toList() is:

List<T> toList()

so if you execute it for a Stream<FancyDodo>, you'll get a List<FancyDodo>, which can't be assigned to a List<Dodo> variable.

I suggest you simply use stream.collect(Collectors.toList()) instead of stream.toList() in this case.

Because Stream.toList is declared to return a List<T>:

default List<T> toList() { ... }

where T is the element type of the stream. I can't really think of an alternative way of declaring toList so that it can return your desired type of list. The best you can do is to accept a List<? super T> as argument, and add the stream elements to it, but that kind of goes against the "aesthetics" of streams - the whole point of this is to be declarative and have little state.

One way you can rewrite your code to make toList return a list of your desired type, is to specify the type of T manually. Right now T is inferred to be FancyDodo due to Stream.of(new FancyDodo()), but you can force T to be Dodo if you want:

Stream.<Dodo>of(new FancyDodo()).toList();

Now T is Dodo, toList will return a List<Dodo>.


The best you can do is to accept a List<? super T> as argument, and add the stream elements to it

Actually, this is kind of what Collector is doing. Notice how collect accepts a Collector<? super T, DoesntMatter, R>, and returns R. That contravariant ? super T is what enables you to use a toList collector like that. Note also that R is a generic parameter of collect, which means that you get to decide what collect returns, as long as you can provide a collector that collects ? super T to R.

Generic type argument resolution happens one method call at a time.

Stream.of(new FancyDodo()) will always resolved T to FancyDodo, so will always result in a Stream<FancyDodo>.

toList() doesn't resolve T, it just uses the already-established T, so the result is always List<FancyDodo>, and List<FancyDodo> is not compatible with List<Dodo>. See: "Is List<Dog> a subclass of List<Animal>? Why are Java generics not implicitly polymorphic?"

collect(Collectors.toList()) has a different T in the Collectors.toList(), that can resolve differently from the T of the Stream. The compiler resolves that T as Dodo, because of the desired return type of List<Dodo>.

Related