When confronted with this kind of situation, I feel the best way to understand the problem is with pure reasoning and logic. Type-inference is a beast that covers an entire chapter of the JLS. Let's forget about ECJ and javac
for the moment, think through the 4 examples and determine whether a given compiler could or should be able to compile it according to the JLS.
So let's consider the signature of collect
:
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
The questions with this signature is: what is R
and how will a compiler be able to determine what R
is?
We can argue that a compiler will be able to infer a type for R
with what we're giving as parameter to collect
. As an example, the first is a Supplier<R>
so if, e. g., we are to give as parameter () -> new StringBuilder()
, a compiler should be able to infer R
as StringBuilder
.
Let's consider the following case:
List<Integer> l = Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
In this example, the 3 parameters of collect
are 3 method-references and we're assigning the result to a List<Integer>
. That's information a compiler could take: we are saying to it that the type used is Integer
.
Okay, so should it compile? Let's consider the 3 arguments to collect
separately:
- The first, in this case, is a
Supplier<ArrayList<Integer>>
and we are giving ArrayList::new
. Can this method-reference refer to an existing method (constructor in this case)? Well yes, it can refer to the empty constructor of ArrayList
(as a lambda - () -> new ArrayList<Integer>()
) because ArrayList<Integer>
can be bound to List<Integer>
. So far so good.
- The second is
BiConsumer<ArrayList<Integer>, ? super Integer>
. Note that T = Integer
here because the Stream is composed of integer literals which are of type int
. We're giving ArrayList::add
, which can refer to add(e)
(as a lambda: (list, element) -> list.add(element)
).
- The third is
BiConsumer<List<Integer>, List<Integer>>
and we're giving ArrayList::addAll
. It can also refer to addAll(c)
: addAll
takes as parameter a Collection<? extends Integer>
and List<Integer>
can be bound to this type.
So basically, with only reasoning, such an expression should compile.
Now, let's consider your 4 cases:
Case 1:
List<Integer> l = Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList<Integer>::addAll);
We're assigning the result of the expression as a List<Integer>
so we're telling a compiler: R
is List<Integer>
here. The difference with the case above is that we're giving the method reference ArrayList<Integer>::addAll
. Let's take a closer look at this. This method-reference is trying to refer to a method name addAll
which would take as parameter a List<Integer>
(our R
) and should be applied to a ArrayList<Integer>
(the type we're explicitely using in the method-reference). Well this is exactly the same as what we concluded in the reasoning above; it should work: R = List<Integer>
can be bound to ArrayList<Integer>
.
Case 2
Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
The difference with the case above is that we're not assigning the result of the expression. So a compiler is left to infer the type based on the supplier: ArrayList::new
, so it should infer it as ArrayList<Object>
.
ArrayList::add
can be bound to BiConsumer<ArrayList<Object>, ? super Integer>
because the add
method of a ArrayList<Object>
can take an Integer
as argument.
ArrayList::addAll
can be bound to BiConsumer<ArrayList<Object>, ArrayList<Object>>
.
So a compiler should be able to compile that.
Case 3
Stream.of(1, 2, 3).collect(ArrayList<Integer>::new, ArrayList::add, ArrayList::addAll);
The difference with case 2 is that we're explicitely telling the compiler that the supplier supplies ArrayList<Integer>
instances, not just ArrayList<Object>
. Does it change anything? It should not, the reasoning made in case 2 still holds here. So it should compile just as well.
Case 4
Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList<Integer>::addAll);
The difference with case 2 is that this time, we're giving ArrayList<Integer>::addAll
. Based from case 2, we know that a compiler inferred R
to be ArrayList<Object>
because of the supplier (that does not has a specific type). This should cause a problem here: ArrayList<Integer>::addAll
tries to reference the method addAll
on a ArrayList<Integer>
but we saw that, for a compiler, this
was inferred as ArrayList<Object>
and an ArrayList<Object>
is not an ArrayList<Integer>
. So this should not compile.
What could we do to make it compile?
- Change the supplier to have a specific
Integer
type.
- Remove the explicit
<Integer>
from the method-reference.
- Write the method reference as
ArrayList::<Integer> addAll
instead.
Conclusion
I tested the examples with Eclipse Mars 4.5.1 and javac
1.8.0_60. The result is that javac
, behaves exactly as with our reasoning concluded: only case 4 is not compiled.
Bottom line, Eclipse has a small bug.