Skip to content
Advertisement

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

Given the following identity functions:

<T> Consumer<T> f(Consumer<T> c) { return c; }          // (1)
<T,R> Function<T,R> f(Function<T, R> c) { return c; }   // (2)

I observe the following behaviour in JDK 11 and JDK 17:

void _void() {}
f(x -> {});                   // okay, dispatches to (1)
f(x -> { return; });          // okay, dispatches to (1)
f(x -> { _void(); });         // okay, dispatches to (1)
f(x -> _void());              // should dispatch to (1)
|  Error:
|  reference to f is ambiguous
|    both method f(java.util.function.Function<java.lang.Object,java.lang.Object>) in  
     and method f(java.util.function.Consumer<java.lang.Object>) in  match

int _one() { return 1; }
f(x -> 1);                    // okay, dispatches to (2)
f(x -> { return 1; });        // okay, dispatches to (2)
f(x -> { return _one(); });   // okay, dispatches to (2)
f(x -> _one());               // should dispatch to (2)
|  Error:
|  reference to f is ambiguous
|    both method <T,R>f(java.util.function.Function<T,R>) in
     and method <T>f(java.util.function.Consumer<T>) in  match

Why can’t the compiler resolve these symbols by using the return type of the expression? The curly brace versions work fine, and I would have thought they would be the more difficult cases. I understand that you can explicity cast the lambda function, but that defeats the purpose of what I am trying to achieve.

Advertisement

Answer

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.

Advertisement