Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unable to understand this Java Stream+Generics example

Could someone help me to understand why this code behaves as described in the comments

// 1) compiles
List<Integer> l = Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList<Integer>::addAll);

/*
 *  2) does not compile
 *  
 *  Exception in thread "main" java.lang.Error: Unresolved compilation problems: 
 *      Type mismatch: cannot convert from Object to <unknown>
 *      The type ArrayList does not define add(Object, Integer) that is applicable here
 *      The type ArrayList does not define addAll(Object, Object) that is applicable here
 */
Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

// 3) compiles
Stream.of(1, 2, 3).collect(ArrayList<Integer>::new, ArrayList::add, ArrayList::addAll);

/*
 *  4) does not compile
 *  
 *  Exception in thread "main" java.lang.Error: Unresolved compilation problems: 
 *      Type mismatch: cannot convert from Object to <unknown>
 *      The type ArrayList does not define add(Object, Integer) that is applicable here
 *      The type ArrayList<Integer> does not define addAll(Object, Object) that is applicable here
 */
Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList<Integer>::addAll);  

It has clearly something to do with the definition of a type in generic methods, it's an information that must be somehow provided... but why is it mandatory? Where and how, syntactically, should I have figured it out from the signature of methods of() and collect()?

public static<T> Stream<T> of(T... values) {...}

<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);
like image 519
Luigi Cortese Avatar asked Nov 24 '15 22:11

Luigi Cortese


2 Answers

Although this is not an answer which analyzes the Lambda spec on http://download.oracle.com/otndocs/jcp/lambda-0_9_3-fr-eval-spec/index.html, I nevertheless tried to find out on what it depends.

Copying two methods from the Stream class:

static class Stream2<T> {

    @SafeVarargs
    @SuppressWarnings("varargs") // Creating a stream from an array is safe
    public static<T> Stream2<T> of(T... values) {
        return new Stream2<>();
    }

     public  <R> R collect(  Supplier<R> supplier,
             BiConsumer<R, ? super T> accumulator,
             BiConsumer<R, R> combiner){return null;}

}

This compiles:

Stream2.of(1,2,3).collect(ArrayList::new, ArrayList::add, ArrayList::addAll );

like OP's (2).

Now changing the collect method to by moving the first argument to the third place

     public  <R> R collect(BiConsumer<R, ? super T> accumulator,
             BiConsumer<R, R> combiner,
             Supplier<R> supplier
     ){return null;}

This still works (5):

 Stream2.of(1,2,3).collect(ArrayList::add, ArrayList::addAll,ArrayList::new );

Also this works (6):

 Stream2.of(1,2,3).collect(ArrayList::add, ArrayList::addAll,ArrayList<Integer>::new );

These don't work (7,8):

 Stream2.of(1,2,3).collect(ArrayList<Integer>::add, ArrayList::addAll,ArrayList::new );
 Stream2.of(1,2,3).collect(ArrayList<Integer>::add, ArrayList<Integer>::addAll,ArrayList::new );

But this works again (9):

 Stream2.of(1,2,3).collect(ArrayList<Integer>::add, ArrayList<Integer>::addAll,ArrayList<Integer>::new );

So i guess when a supplier is annotated with the explicit type argument, it seems to work. When only the consumers are, it does not. But maybe someone else knows why this makes a difference.

EDIT: Trying to use a TestList, it gets even stranger:

public class StreamTest2 {

    public static void main(String[] args) {

        Stream.of(1, 2, 3).collect(TestList::new, TestList::add, TestList<Integer>::addAll);
        Stream.of(1, 2, 3).collect(TestList::new, TestList::add, TestList<Integer>::addAll2);
        Stream.of(1, 2, 3).collect(TestList::new, TestList::add, TestList<Integer>::addAll3);
        Stream.of(1, 2, 3).collect(TestList::new, TestList::add, TestList<Integer>::addAll4);
        Stream.of(1, 2, 3).collect(TestList::new, TestList::add, TestList<Integer>::addAll5);
        Stream.of(1, 2, 3).collect(TestList::new, TestList::add, TestList<Integer>::addAll6);

    }
}

class TestList<T> extends AbstractList<T> {

    @Override
    public T get(int index) {
        return null;
    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public boolean addAll(Collection<? extends T> c) {
        return true;
    }

    public boolean addAll2(TestList<? extends T> c) {
        return true;
    }
    public boolean addAll3(Collection<T> c) {
        return true;
    }

    public boolean addAll4(List<? extends T> c) {
        return true;
    }
    public boolean addAll5(AbstractList<? extends T> c) {
        return true;
    }

    public boolean addAll6(Collection<? extends T> c) {
        return true;
    }

    @Override
    public boolean add(T e) {
        return true;
    }
}

addAll does not work, but addAll2-6 do work. Even addAll6 works, which has the same signature as the original addAll.

like image 131
user140547 Avatar answered Sep 24 '22 21:09

user140547


Whenever you struggle about compiler errors, you should include, which compiler you have used and its version number. And if you have used a compiler other than the standard javac, you should give javac a try and compiler the results.

When you write

List<Integer> l = Stream.of(1, 2, 3)
    .collect(ArrayList::new, ArrayList::add, ArrayList<Integer>::addAll);

the compiler will use the target type List<Integer> for inferring the type R (which matches exactly the target type here). Without a target type like in

Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

the compiler will infer the type R from the supplier and infer ArrayList<Object> instead. Since an ArrayList<Object> is capable of holding Integer instances and provides the necessary add and addAll methods, this construct compiles without problems when using the standard javac. I tried jdk1.8.0_05, jdk1.8.0_20, jdk1.8.0_40, jdk1.8.0_51, jdk1.8.0_60, jdk1.9.0b29, and jdk1.9.0b66 to be sure that there are no version specific bugs involved. I guess, you are using Eclipse, which is known for having problems with the Java 8 type inference.

Similarly, using

Stream.of(1, 2, 3).collect(ArrayList<Integer>::new, ArrayList::add, ArrayList::addAll);

works but now your hint forces the inferred type for R to be ArrayList<Integer>. In contrast

Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList<Integer>::addAll);

does not work as the compiler is inferring ArrayList<Object> for the return type of the supplier which is not compatible with the method ArrayList<Integer>::addAll. But the following would work:

Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList<Object>::addAll);

However, you don’t need any explicit type when using standard javac

like image 33
Holger Avatar answered Sep 20 '22 21:09

Holger