62

The following code surprisingly is compiling successfully:

Consumer<String> p = ""::equals;

This too:

p = s -> "".equals(s);

But this is fails with the error boolean cannot be converted to void as expected:

p = s -> true;

Modification of the second example with parenthesis also fails:

p = s -> ("".equals(s));

Is it a bug in Java compiler or is there a type inference rule I don't know about?

Michael
  • 41,989
  • 11
  • 82
  • 128
Zefick
  • 2,014
  • 15
  • 19
  • Eclipse complains as well, so probably *not* a compiler bug. – GhostCat Aug 02 '17 at 12:31
  • 2
    `s -> true;` and `s -> ("".equals(s))` is interpreted as `s -> {return true;}` or `s -> { return ("".equals(s));}` And they are not a `Function`. `p = s -> true;` this no make sense, use `p = s -> {};` instead. – David Pérez Cabrera Aug 02 '17 at 12:33
  • @DavidPérezCabrera I understand why last two lines of code are not compiling. It is not clear as the second two works. – Zefick Aug 02 '17 at 12:39
  • 2
    @Zefick IMHO it's a license of compiler, It must interpret you can ignore the return value. to avoid unnecessary verbosity, if It don't do that, you can't use `p = ""::equals;` and you must to define `p = s -> "".equals(s);` as this: `p = s -> {"".equals(s);};`. IMHO It's a good decision. – David Pérez Cabrera Aug 02 '17 at 12:46
  • 7
    See [Why does a Java method reference with return type match the Consumer interface?](https://stackoverflow.com/q/37308294/2711488) or [Lambda 'special void-compatibility rule' - statement expression](https://stackoverflow.com/q/41482574/2711488). Regarding, why round brackets change the behavior, see [Java 8 Lambda Syntax change](https://stackoverflow.com/a/24147329/2711488)… – Holger Aug 02 '17 at 15:26

3 Answers3

83

First, it's worth looking at what a Consumer<String> actually is. From the documentation:

Represents an operation that accepts a single input argument and returns no result. Unlike most other functional interfaces, Consumer is expected to operate via side-effects.

So it's a function that accepts a String and returns nothing.

Consumer<String> p = ""::equals;

Compiles successfully because equals can take a String (and, indeed, any Object). The result of equals is just ignored.*

p = s -> "".equals(s);

This is exactly the same, but with different syntax. The compiler knows not to add an implicit return because a Consumer should not return a value. It would add an implicit return if the lambda was a Function<String, Boolean> though.

p = s -> true;

This takes a String (s) but because true is an expression and not a statement, the result cannot be ignored in the same way. The compiler has to add an implicit return because an expression can't exist on its own. Thus, this does have a return: a boolean. Therefore it's not a Consumer.**

p = s -> ("".equals(s));

Again, this is an expression, not a statement. Ignoring lambdas for a moment, you will see the line System.out.println("Hello"); will similarly fail to compile if you wrap it in parentheses.


*From the spec:

If the body of a lambda is a statement expression (that is, an expression that would be allowed to stand alone as a statement), it is compatible with a void-producing function type; any result is simply discarded.

**From the spec (thanks, Eugene):

A lambda expression is congruent with a [void-producing] function type if ... the lambda body is either a statement expression (§14.8) or a void-compatible block.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Michael
  • 41,989
  • 11
  • 82
  • 128
  • So the key moment is that result of `equals` can be just ignored. Quite unexpectedly for such strong typed language. – Zefick Aug 02 '17 at 12:55
  • 3
    @Zefick Yes, the key difference is that statements may or may not have an implicit `return` (based on inference), while expressions always have an implicit `return`. It results in nicer syntax. – Michael Aug 02 '17 at 13:00
  • 2
    @Michael Someday I would love to know *why* there is a distinction between statements and expressions in the language. I really don't get it, just let everything be statements... – fps Aug 02 '17 at 17:43
  • 4
    @FedericoPeraltaSchaffner it's not a compiler that defines this, but JLS :) (to which compilers have to conform). If all expressions where statements compilers wouldn't be able to report a lot of errors. If all results from non-void functions would have to be used, lots of unnecessary assignments / additional local variables would be needed, just think of `StringBuilder.append(..)` for a start. It's been like this for a long time, possibly since Java 1.0 – Stephan Herrmann Aug 02 '17 at 22:28
12

I think the other answers complicate the explanation by focusing on lambdas whereas their behavior in this case is similar to the behavior of manually implemented methods. This compiles:

new Consumer<String>() {
    @Override
    public void accept(final String s) {
        "".equals(s);
    }
}

whereas this does not:

new Consumer<String>() {
    @Override
    public void accept(final String s) {
        true;
    }
}

because "".equals(s) is a statement but true is not. A lambda expression for a functional interface returning void requires a statement so it follows the same rules as a method's body.

Note that in general lambda bodies don't follow exactly the same rules as method bodies - in particular, if a lambda whose body is an expression implements a method returning a value, it has an implicit return. So for example, x -> true would be a valid implementation of Function<Object, Boolean>, whereas true; is not a valid method body. But in this particular case functional interfaces and method bodies coincide.

Reinstate Monica
  • 2,420
  • 14
  • 23
  • 4
    "there is nothing special about lambdas here". You're mostly right, however `s -> "".equals(s)` can satisfy both `Consumer` **and** `Function` which I believe was a source of the confusion. – Michael Aug 02 '17 at 14:53
  • 1
    I think it's also worth mentioning that `s -> true` *can* be a valid lambda (e.g. `Predicate foo = s -> true;`) whereas `true;` on its own will never compile. – Michael Aug 08 '17 at 10:29
8
s -> "".equals(s)

and

s -> true

don't rely on same function descriptors.

s -> "".equals(s) may refer either String->void or String->boolean function descriptor.
s -> true refers to only String->boolean function descriptor.

Why ?

  • when you write s -> "".equals(s), the body of the lambda : "".equals(s) is a statement that produces a value.
    The compiler considers that the function may return either void or boolean.

So writing :

Function<String, Boolean> function = s -> "".equals(s);
Consumer<String> consumer = s -> "".equals(s);

is valid.

When you assign the lambda body to a Consumer<String> declared variable, the descriptor String->void is used.
Of course, this code doesn't make much sense (you check the equality and you don't use the result) but the compiler doesn't care.
It is the same thing when you write a statement : myObject.getMyProperty() where getMyProperty() returns a boolean value but that you don't store the result of it.

  • when you write s -> true, the body of the lambda : true is a single expression .
    The compiler considers that the function returns necessarily boolean.
    So only the descriptor String->boolean may be used.

Now, come back to your code that doesn't compile.
What are you trying to do ?

Consumer<String> p = s -> true;

You cannot. You want to assign to a variable that uses the function descriptor Consumer<String> a lambda body with the String->void function descriptor. It doesn't match !

davidxxx
  • 125,838
  • 23
  • 214
  • 215
  • I actually think `s -> "".equals(s)` is a *statement expression* according to the JLS - that means an expression is allowed to be used as a statement. – Eugene Aug 07 '17 at 11:41
  • here is the link : https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.8 – Eugene Aug 07 '17 at 11:42