5

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);
    }
}
Basilevs
  • 22,440
  • 15
  • 57
  • 102
ning
  • 61
  • 4

1 Answers1

6

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.

ajb
  • 31,309
  • 3
  • 58
  • 84
  • 2
    I think [15.13.2](http://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.13.2) is more relevant in this case -- "A method reference expression is compatible in an assignment context [...] with a target type T [...] if [...] The result of the function type [of T] is void" – Ismail Badawi Dec 28 '14 at 07:19
  • ok, i think the explanation is fairly clear. Not sure why this is accepted, since it will cause bug in most cases. Strong type checking should prevent this kind of bug. – ning Dec 28 '14 at 11:03
  • 6
    You are correct on the motivation. Just as you can invoke a method and discard the return type, you can convert a result-bearing method to a void-returning functional interface. The previous commenter worries that this flexibility invites bugs, but the alternative was worse -- you couldn't even convert `aList::add` to a `Consumer`, since `add` returns something - and this would be really annoying, if you couldn't say `x.forEach(list::add)`! Either solution was going to annoy someone. This way was judged to be the lesser of evils. And really, it wasn't even a close call. – Brian Goetz Dec 28 '14 at 17:13
  • @BrianGoetz Thanks for pointing this out. I hadn't thought about "functions" whose main purpose is not to return a value but to make some important change, but that also return some kind of status, or an object for chaining, or something else. Maybe some future language will make a distinction between those types of functions. (A preliminary version of Ada distinguished between "functions" which were not allowed to have side-effects, and "value-returning procedures". That didn't make it into the final 1983 standard.) – ajb Dec 29 '14 at 00:54