8
public class Test {

    static List<Object> listA = new ArrayList<>();

    public static void main(final String[] args) {
        final List<TestClass> listB = new ArrayList<>();
        listB.add(new TestClass());

        // not working
        setListA(listB);

        // working
        setListA(listB.stream().collect(Collectors.toList()));

        System.out.println();
    }

    private static void setListA(final List<Object> list) {
        listA = list;
    }

}

why does it work with streams and does not work for the simple set?

holi-java
  • 29,655
  • 7
  • 72
  • 83
user817795
  • 165
  • 1
  • 11
  • 4
    `setListA(Collections.unmodifiableList(listB))` would also work without the overhead of creating a stream. – Radiodef Jun 14 '17 at 14:16
  • 1
    @Radiodef That would do something that is *completely different* from the posted code. (Not related to the type inference etc., but wanted to mention it. passing in a `new ArrayList(listB)` would be closer to what the posted code does, and would work as well) – Marco13 Jun 15 '17 at 11:56

3 Answers3

8

For the first case, it fails because List<TestClass> is not a subtype of List<Object>.1

For the second case, we have the following method declarations:

interface Stream<T> {
    // ...
    <R, A> R collect(Collector<? super T, A, R> collector)
}

and:

class Collectors {
    // ...
    public static <T> Collector<T, ?, List<T>> toList()
}

This allows Java to infer the generic type parameters from the context.2 In this case List<Object> is inferred for R, and Object for T.

Thus your code is equivalent to this:

Collector<Object, ?, List<Object>> tmpCollector = Collectors.toList();
List<Object> tmpList = listB.stream().collect(tmpCollector);
setListA(tmpList);

1. See e.g. here.

2. See e.g. here or here.

Oliver Charlesworth
  • 267,707
  • 33
  • 569
  • 680
  • Does that then mean that the problem is an implicit cast to `Object` in the first "not working" case? – Thomas Jun 14 '17 at 14:16
  • 2
    @Thomas - It's simply that `List` is not a subtype of `List`, and thus the assignment fails. – Oliver Charlesworth Jun 14 '17 at 14:17
  • 1
    Can you explain in more detail why List is inferred for R and Object for T? Especially in the context that `listB.stream()` actually returns a `Stream`, so why would `listB.stream().collect(...)` use Object instead? – Klitos Kyriacou Jun 14 '17 at 14:18
  • @KlitosKyriacou - The rules are complex, and I don't claim to understand them well enough to be able to explain them unambiguously. See http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html for a better explanation than I could ever give! – Oliver Charlesworth Jun 14 '17 at 14:19
  • 4
    @OliverCharlesworth Having been a teacher, myself, I always appreciate a clearly knowledgeable engineer saying "I don't know." That's an important thing and I wish more of the people in our world did it. – Thomas Jun 14 '17 at 14:25
  • 2
    @KlitosKyriacou Type inference uses something called the *target type* of an expression. For a method invocation expression, the target type is typically whatever the return value is assigned to, in this case a parameter of another method. The target type of `collect` is `List` of `setListA` and the target type of `toList` is `Collector super T, A, R>` of `collect`. I think that Javac must be inferring the type of `collect` first here, just based on the fact that this code compiles, but it's been awhile since I've read this section of the JLS. – Radiodef Jun 14 '17 at 14:40
  • 1
    @Radiodef: [your example using `unmodifiableList`](https://stackoverflow.com/questions/44547122/java-add-list-of-specific-class-to-list-of-java-lang-object-works-with-java-8-st#comment76085259_44547122) would be a simpler starting point when trying to explain the target type inference. The stream/toList collector approach creates a copy, which would be equivalent to `setListA(new ArrayList<>(listB));` which also works since Java 8. In older Java versions, `setListA(new ArrayList(listB));` would be required. – Holger Jun 15 '17 at 07:54
2

This line

setListA(listB);

doesn't work because List in Java is invariant, meaning List<TestClass> doesn't extends List<Object> when TestClass extends Object. More details here

This line

setListA(listB.stream().collect(Collectors.toList()));

works because Java infer Object for Collector's generic type from this method signature setListA(final List<Object> list) and so you actually pass List<Object> there

ikryvorotenko
  • 1,393
  • 2
  • 16
  • 27
  • 1
    I like your explanation of invariance in this case. I was actually wondering about this, thank you. – Thomas Jun 14 '17 at 14:27
1

the type parameters of Java Generic is invariance which means it can't be inherited as type parameters class hierarchy. The common parent of List<TestClass> and List<Object> is List<?>.

generic inheritance

you can see detailed answer about java generic wildcard from kotlin & java. for example:

 List<String> strings = new ArrayList<String>();
 List<CharSequence> sequences = strings; // can't work
 List<? extends CharSequence> parent1 = strings; // works fine
 List<?> parent2 = strings; // works fine
 //   ^--- is equaivlent to  List<? extends Object>

the streams approach is transform a List<TestClass> to List<Object>. if you want it works without transform a List to another List by stream. your methods signature should be as below, and the Collection#addAll also does it in java:

List<?> listA = new ArrayList<>();

private static void setListA(List<?> list) {
    listA = list;
}
holi-java
  • 29,655
  • 7
  • 72
  • 83