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

3

23

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 :)

Listlessness answered 13/5, 2021 at 9:58 Comment(5)
Try Stream.<Dodo>of(new FancyDodo()). With that factory method, you get Stream<FancyDodo>, which is not compatible with Stream<Dodo>Magnetic
Interesting question, as an end-user I would have expected it to behave more like the .collect(Collectors.toList()) unless the documentation could call that out. Another interesting line is final List<Dodo> fancyDodosAgain = Arrays.asList(Stream.of(new FancyDodo()).toArray(Dodo[]::new))Olszewski
Because it returns a List<T>, where T is whatever it is a stream of. If you want a more abstract type, that's easy: List<Dodo> dodos = fancyDodos.stream().map(d -> (Dodo) d).toList()Displayed
Alternately, you could say: List<? extends Dodo> = fancyDodos.stream().toList(), since a List<FancyDodo> is a List<? extends Dodo>.Displayed
@Olszewski that’s just another case of the impossibility to declare that a method may return a broader type than another type parameter (hypothetical <U super T> List<U> toList()). This syntax does not exist because when Generics were introduced, the creators thought that this was rarely needed. It works with collect, because the toList collector introduces its own type variable which gets mapped to the Stream’s type parameter with a super bound at the collect call. Edit: oh, just saw this commentRagan
A
22

.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.

Androgynous answered 13/5, 2021 at 10:10 Comment(3)
follow up to the authors if they reach out here, can we try to make it similar in behavior with Collectors.toList?Olszewski
I should have mentioned JDK authors. But let the comment be if they happen to visit. I also now realize that the first impedance in making it work might be the underlying call to toArray that is for certain reasons considered in (default)implementation for the performance of this API. But to add to the questions, the implementation for e.g. ReferencePipeline could still do away with that return SharedSecrets.getJavaUtilCollectionAccess().listFromTrustedArrayNullsAllowed(this.toArray(Object[]::new));... maybe?Olszewski
@Olszewski Unfortunately there's no way to do this. You might think something like <U super T> List<U> toList() would work, but alas, having super in that type bound is illegal.Krug
D
20

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.

Drilling answered 13/5, 2021 at 10:14 Comment(5)
"I can't really think of an alternative way of declaring toList so that it can return your desired type of list." Surely default <R super T> List<R> toList() would work?Concede
Wait, since when can you use super in generic constraints!? Is that a Java 16 feature? @ConcedeDrilling
Since Generics were introduced in Java 5. It's in the type signature for collect: docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/…Concede
@Concede Did you confuse wildcards with generic constraints? collect uses a wildcard ? super T, but your suggested signature uses super T as a generic constraint. You can do default List<? super T> toList(), sure, but not default <R super T> List<R> toList(). Did you try writing such a method yourself? I get an "unexpected token" error.Drilling
I guess I am confused. You're right, I can't get it to work either. I was not aware of this limitation of generics.Concede
P
8

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>.

Pintle answered 13/5, 2021 at 10:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.