Java can correctly deduce SAM types and allows me to pass a lambda as a substitute to their implementation, but the same mechanism fails when I try to convert one SAM type to another that has the same signature:
public static Supplier<String> saySomething(){
return () -> "Hello";
}
@FunctionalInterface
interface Greeter {
String greet();
}
public static Greeter getGreeting1(){
// this compiles and runs just fine
return () -> saySomething().get();
}
public static Greeter getGreeting2(){
// this fails at compile:
// java: incompatible types: java.util.function.Supplier<java.lang.String> cannot be converted to Greeter
return saySomething();
}
public static Greeter getGreeting3(){
// this compiles without warnings(probably only because a Supplier<String> might
// also implement the Greeter interface, by chance) but if you call it will fail
// at runtime with a java.lang.ClassCastException
return (Greeter) saySomething();
}
I'm trying to understand why the JVM won't let a lambda be treated as one thing here and another thing there, since it just needs to make another SAM conversion for that. I suspect it's because lambdas are promoted to anonymous classes under to hood, but can someone give a definitive answer?
Also, is there any workaround(except wrapping each SAM conversion in another lambda, as in the first example) to allow it to work?
Lambda expressions are a Java language artifact, not a JVM feature.
There are several compile-time features involved, which are not present at runtime and also would be too expensive to perform at runtime.
The compiler uses type inference, incorporating the generic type system, to determine which interface(s) to implement.
It then checks the potentially complex type hierarchy of the interface, including which method overrides which, again also considering the rules of the generic type system, to eventually determine which abstract methods (excluding private
, default
, and static
methods) which do not match a public
method of java.lang.Object
are left to implement.
If there is only one method to implement, the erasure of the method and all Bridge Methods are identified, for inclusion in the recipe for constructing an instance of the functional interface(s) inferred in the first step.
The overall operation is an object creation, though the implementation may reuse existing objects:
At run time, evaluation of a lambda expression is similar to evaluation of a class instance creation expression, insofar as normal completion produces a reference to an object.
In contrast, a type cast is a type checking expression:
A cast expression […] checks, at run time, that a reference value refers to an object either whose class is compatible with a specified reference type or list of reference types […]
The result, if successful, is still referencing the same object. So if you have
interface A {
int test();
}
interface B extends A {
default int test() {
return method2() + 1;
}
int method2();
}
B b1 = () -> 10;
A a1 = (A)b1;
System.out.println(a1.test());
It will print 11
, as the variable a1
still contains a reference to the same B
instance as b1
and its test()
method is implemented as method2() + 1
.
In contrast,
A a2 = () -> 10;
System.out.println(a2.test());
will print 10
and hence, it’s clear that this can’t be the same object as b1
above, regardless of the way the object was created (i.e. the same lambda expression () -> 10
).
A conversion of this A
instance to B
like B b2 = (B)a2;
by assuming its method2()
to be implemented like test()
due to the compatible functional signature can’t be possible, as it would cause contradicting behavior regarding the result’s test()
method (If still not convinced, think about what would happen, if we cast b2
back to A
again).
In contrast, when you use a method reference like
B b2 = a2::test;
you make it explicit that b2
will be a different object than a2
and b2
’s functional method (method2()
) will exhibit the same behavior as a2
’s functional method (test()
). So it’s consistent that b2
’s test()
method will have a different behavior than a2
’s test()
method.