Why Do Consumers Accept Lambdas with Statement Bodies But Not Expression Bodies

Why do Consumers accept lambdas with statement bodies but not expression bodies?

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.

Why doesn't for-each method in java not throw an exception when a Function type argument is passed instead of Consumer?

Regarding why the first line works: there's an explanation in the specification:

Generally speaking, a lambda of the form () -> expr, where expr is a statement expression, is interpreted as either () -> { return expr; } or () -> { expr; }, depending on the target type.

The above comes with the following example (good coincidence, this is very similar to your example):

// Consumer has a void result
java.util.function.Consumer<String> c = s -> list.add(s);

This simply means that the compiler ignores the return type for the expression, as though your code were simply this (which is valid for a void method):

Stream.of(1, 2, 3, 4).forEach(a -> {
a.equals(1);
});

And regarding the second line, the spec says:

A block lambda body is void-compatible if every return statement in the block has the form return;.

In your case, though, {return a.equals(1);} does not meet this rule. Void methods don't return a value.

An easy way to understand this is to consider that the compiler applies method body validation rules (such that the body must be compatible with the declaration public void accept(T t)) - as mentioned in the tutorial

Why can this lambda expression assigned to different functional interfaces?

The relevant part of the spec is Sec 15.27.3 (emphasis mine):

A lambda expression is congruent with a function type if all of the following are true:

  • The function type has no type parameters.

  • The number of lambda parameters is the same as the number of parameter types of the function type.

  • If the lambda expression is explicitly typed, its formal parameter types are the same as the parameter types of the function type.

  • If the lambda parameters are assumed to have the same types as the function type's parameter types, then:

    • If the function type's result is void, the lambda body is either a statement expression (§14.8) or a void-compatible block.

    • If the function type's result is a (non-void) type R, then either i) the lambda body is an expression that is compatible with R in an assignment context, or ii) the lambda body is a value-compatible block, and each result expression (§15.27.2) is compatible with R in an assignment context.

Your lambda body is a statement expression, and the function type's result is void.

In other words, it would be fine for you to write:

return0();

and ignore the return value in "regular" code, so it's fine to ignore the result value in a lambda too.


In terms of the question over ambiguity of overloads, there is no ambiguity in this case (it's easy to construct a case where there is ambiguity, e.g. another overload with a parameter that looks like Supplier but is a different interface, i.e. takes no parameters, returns a value).

You would have to read the spec in detail for the precise reasoning, but I think the most relevant section is Sec 15.12, which describes method invocation expressions, and the most useful quote from that is in Sec 15.12.2.5, which deals with selecting the most-specific overload:

The informal intuition is that one method is more specific than another if any invocation handled by the first method could be passed on to the other one without a compile-time error.

You can use a Supplier<Integer> in place of a Runnable (with a bit of a hand-wavy fudge) because you can simply ignore the return value; you can't use a Runnable in place of a Supplier<Integer> because it doesn't have a return value.

So a method taking the Supplier<Integer> is more specific than the method taking the Runnable, hence that is the one which is invoked.

Why does this Java 8 lambda fail to compile?

Your lambda needs to be congruent with BiConsumer<String, String>. If you refer to JLS #15.27.3 (Type of a Lambda):

A lambda expression is congruent with a function type if all of the following are true:

  • [...]
  • If the function type's result is void, the lambda body is either a statement expression (§14.8) or a void-compatible block.

So the lambda must either be a statement expression or a void compatible block:

  • A constructor invocation is a statement expression so it compiles.
  • A string literal isn't a statement expression and is not void compatible (cf. the examples in 15.27.2) so it does not compile.

Lambda Expression that has return type as void can compile with wrapper but not compile with primitive

Wrapper vs primitive is not the cause. The following will not compile either:

Integer i = 5;
A b = () -> i;

This is covered in JLS 15.27.3:

If the function type's result is void, the lambda body is either a statement expression (§14.8) or a void-compatible block.

5 is neither a statement not a void-compatible block. So A a = () -> 5; will not compile.

The reason for this restriction is that void-returning lambda only makes sense if it operates by a side-effect. Simply returning a value like () -> 5 has no side effects and therefore such a lambda expression is definitely a bug.

What would be the problem in providing this keyword for Java's lambda body?

The Java Language Specification 15.27.2 says:

Unlike code appearing in anonymous class declarations, the meaning of names and the this and super keywords appearing in a lambda body, along with the accessibility of referenced declarations, are the same as in the surrounding context (except that lambda parameters introduce new names).

The transparency of this (both explicit and implicit) in the body of a lambda expression - that is, treating it the same as in the surrounding context - allows more flexibility for implementations, and prevents the meaning of unqualified names in the body from being dependent on overload resolution.

Practically speaking, it is unusual for a lambda expression to need to talk about itself (either to call itself recursively or to invoke its other methods), while it is more common to want to use names to refer to things in the enclosing class that would otherwise be shadowed (this, toString()). If it is necessary for a lambda expression to refer to itself (as if via this), a method reference or an anonymous inner class should be used instead.

Why does Java type inference fail to distinguish between Function and Consumer?

x -> _void() and x -> one() are expected to be compatible with Consumer<T> (with the result of one() to be discarded).

When the lambda body is of a block type, the compiler additionally checks the "return" compatibility.
The JLS is rather explicit about void/value compatibility for block bodies:

A block lambda body is void-compatible if every return statement in the block has the form return;.
A block lambda body is value-compatible if it cannot complete normally (§14.21) and every return statement in the block has the form return Expression;.

While that doesn't say why the single-expression bodies fail, it says exactly why block bodies compile: the compiler looks at the return forms to judge on those bodies' compatibility with Consumer or Function (in this case).

For the method invocation expressions, the fact that this is allowed:

Consumer<Integer> c = x -> one(); //discarded result
Function<T, Integer> f = x -> one(); //returned result

doesn't enable the compiler to resolve the conflict that you observed. You can rewrite the same lambda expression with block bodies to resolve the conflict, and that's simply because block bodies are checked differently, by spec.


I guess I'm trying to say that the more natural question is "why block bodies compile at all in this case", given that we normally don't expect return types (forms?) to participate in overload resolution. But lambda expressions' congruence with types is something else, isn't it... I think this (that block type helps target type inference) is the special behavior.



Related Topics



Leave a reply



Submit