Generic parameter returns List<ChildInterface> in stream, but direct method call returns List<Parent>

Question

For setup, I have :

interface Parent

interface Child1 extends Parent

interface Child2 extends Parent

And elsewhere I have:

public class MyClass {
    private List<Child1> child1List = new ArrayList<>();

    public List<Parent> getChild1List(Contact contact) {
        return child1List.parallelStream()
                         .filter(m -> m.getContacts().contains(contact))
                         .sorted(Comparator.comparing(Parent::getParentField))
                         .collect(Collectors.toList());
    }
}

When I do it this way, getChild1List returns a List<Parent> (Shouldn't it return List<Child1>?)

Later, I found that stream to be useful for other methods, so I extracted it and built a generic method with it. I have multiple interfaces that extend Parent, so I did as follows:

    private <T extends Parent> List<T> returnsListByContact(List<T> childList, Contact contact) {
        return childList.parallelStream()
                        .filter(m -> m.getContacts().contains(contact))
                        .sorted(Comparator.comparing(Parent::getParentField))
                        .collect(Collectors.toList());
    }

and getChild1List(Contact contact) became:

    public List<Parent> getChild1List(Contact contact) {
        return returnsListByContact(child1List, contact);
    }

but now it doesn't like this, citing that getChild1List is returning a List<Child1>. I don't understand why, as the implementation of the stream was not altered at all - except that the childList which starts it came through a generic parameter, rather than a direct call to the private member field of MyClass.

So why do they return two different things?


Show source
| generics   | list   | java-8   | lambda   | generic-list   2017-01-06 06:01 1 Answers

Answers ( 1 )

  1. 2017-01-06 09:01

    (The example is confusing. Is Meeting really Parent?)

    In the first version of getChild1List, the collect(toList()) method is called on a Stream<Child1> and its target type -- determined by the return type of getChild1List -- is List<Parent>. This works, because it's allowed for a stream of type T to be collected by a collector of a supertype of T. More specifically, you're adding instances of type Child1 to a List<Parent> which is type-safe and is permitted. You could just as well change the declaration of getChild1List() to return List<Child1> instead of List<Parent>.

    You can see where the variance is allowed by looking at the declaration of collect() in Stream<T>:

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

    The ? super T is what allows the variance.

    Your declaration of returnsListByContact,

    <T extends Parent> List<T> returnsListByContact(List<T> childList, ...)
    

    does not allow variance. It takes a parameter of type List<T> and returns a List<T>. The parameter and return type must be identical. That's why there's a mismatch when you pass in a List<Child1> and try to return it from a method whose return type is List<Parent> -- those types are incompatible.

    To fix this, you need to add some variance to your declaration of returnsListByContact. Here's how I'd do it:

    <T extends Parent> List<T> returnsListByContact(List<? extends T> childList, ...)
    

    This allows you to return a list of some type while passing in a list of some subtype, in this case returning List<Parent> while passing in List<Child1>, which is what I think you want.

◀ Go back