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

55 views Asked by At

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?

1

There are 1 answers

3
Stuart Marks On BEST ANSWER

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