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

Zabuzard
  • 25,064
  • 8
  • 58
  • 82
TTT
  • 6,505
  • 10
  • 56
  • 82
  • 11
    Try `Stream.of(new FancyDodo())`. With that factory method, you get `Stream`, which is not compatible with `Stream` – ernest_k May 13 '21 at 10:01
  • 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 fancyDodosAgain = Arrays.asList(Stream.of(new FancyDodo()).toArray(Dodo[]::new))` – Naman May 13 '21 at 10:09
  • 3
    Because it returns a `List`, where `T` is whatever it is a stream of. If you want a more abstract type, that's easy: `List dodos = fancyDodos.stream().map(d -> (Dodo) d).toList()` – Brian Goetz May 13 '21 at 18:00
  • 3
    Alternately, you could say: `List extends Dodo> = fancyDodos.stream().toList()`, since a `List` is a `List extends Dodo>`. – Brian Goetz May 13 '21 at 18:53
  • 2
    @Naman that’s just another case of the impossibility to declare that a method may return a broader type than another type parameter (hypothetical ` List 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 comment](https://stackoverflow.com/questions/67517262/#comment119348250_67517411)… – Holger May 17 '21 at 07:44

3 Answers3

21

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

Eran
  • 387,369
  • 54
  • 702
  • 768
  • 2
    follow up to the authors if they reach out here, can we try to make it similar in behavior with `Collectors.toList`? – Naman May 13 '21 at 10:14
  • 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? – Naman May 13 '21 at 10:26
  • 8
    @Naman Unfortunately there's no way to do this. You might think something like ` List toList()` would work, but alas, having `super` in that type bound is illegal. – Stuart Marks May 13 '21 at 16:19
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.

Naman
  • 27,789
  • 26
  • 218
  • 353
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • "I can't really think of an alternative way of declaring toList so that it can return your desired type of list." Surely `default List toList()` would work? – MikeFHay May 13 '21 at 10:23
  • Wait, since when can you use `super` in generic constraints!? Is that a Java 16 feature? @MikeFHay – Sweeper May 13 '21 at 10:24
  • Since Generics were introduced in Java 5. It's in the type signature for `collect`: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/Stream.html#collect(java.util.stream.Collector) – MikeFHay May 13 '21 at 10:27
  • 4
    @MikeFHay 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 List toList()`. Did you try writing such a method yourself? I get an "unexpected token" error. – Sweeper May 13 '21 at 10:29
  • 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. – MikeFHay May 13 '21 at 10:34
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>.

Andreas
  • 154,647
  • 11
  • 152
  • 247