I have a weird scenario where type inference isn't working as I'd expect when using a lambda expression. Here's an approximation of my real scenario:
static class Value<T> {
}
@FunctionalInterface
interface Bar<T> {
T apply(Value<T> value); // Change here resolves error
}
static class Foo {
public static <T> T foo(Bar<T> callback) {
}
}
void test() {
Foo.foo(value -> true).booleanValue(); // Compile error here
}
The compile error I get on the second to last line is
The method booleanValue() is undefined for the type Object
if I cast the lambda to Bar<Boolean>
:
Foo.foo((Bar<Boolean>)value -> true).booleanValue();
or if I change the method signature of Bar.apply
to use raw types:
T apply(Value value);
then the problem goes away. The way I'd expect this to work is that:
Foo.foo
call should infer a return type ofboolean
value
in the lambda should be inferred toValue<Boolean>
.
Why doesn't this inference work as expected and how can I change this API to make it work as expected?
Best Answer
Under the Hood
Using some hidden
javac
features, we can get more information about what's happening:This is a lot of information, let's break it down.
phase: method applicability phase
actuals: the actual arguments passed in
type-args: explicit type arguments
candidates: potentially applicable methods
actuals is
<none>
because our implicitly typed lambda is not pertinent to applicability.The compiler resolves your invocation of
foo
to the only method namedfoo
inFoo
. It has been partially instantiated toFoo.<Object> foo
(since there were no actuals or type-args), but that can change at the deferred-inference stage.instantiated signature: the fully instantiated signature of
foo
. It is the result of this step (at this point no more type inference will be made on the signature offoo
).target-type: the context the call is being made in. If the method invocation is a part of an assignment, it will be the left hand side. If the method invocation is itself part of a method invocation, it will be the parameter type.
Since your method invocation is dangling, there is no target-type. Since there is no target-type, no more inference can be done on
foo
andT
is inferred to beObject
.Analysis
The compiler does not use implicitly typed lambdas during inference. To a certain extent, this makes sense. In general, given
param -> BODY
, you will not be able to compileBODY
until you have a type forparam
. If you did try to infer the type forparam
fromBODY
, it might lead to a chicken-and-egg type problem. It's possible that some improvements will be made on this in future releases of Java.Solutions
Foo.<Boolean> foo(value -> true)
This solution provides an explicit type argument to
foo
(note thewith type-args
section below). This changes the partial instantiation of the method signature to(Bar<Boolean>)Boolean
, which is what you want.Foo.foo((Value<Boolean> value) -> true)
This solution explicitly types your lambda, which allows it to be pertinent to applicability (note
with actuals
below). This changes the partial instantiation of the method signature to(Bar<Boolean>)Boolean
, which is what you want.Foo.foo((Bar<Boolean>) value -> true)
Same as above, but with a slightly different flavor.
Boolean b = Foo.foo(value -> true)
This solution provides an explicit target for your method call (see
target-type
below). This allows the deferred-instantiation to infer that the type parameter should beBoolean
instead ofObject
(seeinstantiated signature
below).Disclaimer
This is the behavior that's occurring. I don't know if this is what is specified in the JLS. I could dig around and see if I could find the exact section that specifies this behavior, but type inference notation gives me a headache.
This also doesn't fully explain why changing
Bar
to use a rawValue
would fix this issue:For some reason, changing it to use a raw
Value
allows the deferred instantiation to infer thatT
isBoolean
. If I had to speculate, I would guess that when the compiler tries to fit the lambda to theBar<T>
, it can infer thatT
isBoolean
by looking at the body of the lambda. This implies that my earlier analysis is incorrect. The compiler can perform type inference on the body of a lambda, but only on type variables that only appear in the return type.