Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 collect vs reduce

As known, when doing the accumulation, "reduce" always returns a new immutable object while "collect" will make changes on a mutable object.

However when I accidentally assign one method reference to both reduce and collect method, it compiles without any errors. Why?

Take a look at the following code:

public class Test {
    @Test
    public void testReduce() {

        BiFunction<MutableContainer,Long,MutableContainer> func =
            MutableContainer::reduce;

        // Why this can compile?
        BiConsumer<MutableContainer,Long> consume =
            MutableContainer::reduce;

        // correct way:
        //BiConsumer<MutableContainer,Long> consume =
        //  MutableContainer::collect;


        long param=10;

        MutableContainer container = new MutableContainer(0);


        consume.accept(container, param);
        // here prints "0",incorrect result,
        // because here we expect a mutable change instead of returning a immutable value
        System.out.println(container.getSum());

        MutableContainer newContainer = func.apply(container, param);
        System.out.println(newContainer.getSum());
    }
}

class MutableContainer {
    public MutableContainer(long sum) {
        this.sum = sum;
    }

    public long getSum() {
        return sum;
    }

    public void setSum(long sum) {
        this.sum = sum;
    }

    private long sum;

    public MutableContainer reduce(long param) {
        return new MutableContainer(param);
    }

    public void collect(long param){
        this.setSum(param);
    }
}
like image 463
ning Avatar asked Dec 28 '14 06:12

ning


1 Answers

Basically, the question reduces to this: BiConsumer is a functional interface whose function is declared like this:

void accept(T t, U u)

You've given it a method reference with the correct parameters, but the wrong return type:

public MutableContainer reduce(long param) {
    return new MutableContainer(param);
}

[The T parameter is actually the this object when reduce is called, since reduce is an instance method and not a static method. That's why the parameters are correct.] The return type is MutableContainer and not void, however. So the question is, why does the compiler accept it?

Intuitively, I think this is because a method reference is, more or less, equivalent to an anonymous class that looks like this:

new BiConsumer<MutableContainer,Long>() {
    @Override
    public void accept(MutableContainer t, Long u) {
         t.reduce(u);
     }
}

Note that t.reduce(u) will return a result. However, the result is discarded. Since it's OK to call a method with a result and discard the result, I think that, by extension, that's why it's OK to use a method reference where the method returns the result, for a functional interface whose method returns void.

Legalistically, I believe the reason is in JLS 15.12.2.5. This section is difficult and I don't fully understand it, but somewhere in this section it says

If e is an exact method reference expression ... R2 is void.

where, if I read it correctly, R2 is the result type of a functional interface method. I think this is the clause that allows a non-void method reference to be used where a void method reference is expected.

(Edit: As Ismail pointed out in the comments, JLS 15.13.2 may be the correct clause here; it talks about a method reference being congruent with a function type, and one of the conditions for this is that the result of the function type is void.)

Anyway, that should hopefully explain why it compiles. Of course, the compiler can't always tell when you're doing something that will produce incorrect results.

like image 141
ajb Avatar answered Oct 16 '22 23:10

ajb