Search code examples
javalambdajvmfunctional-interface

Java - Why are SAM types with matching signature not interchangable?


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?


Solution

  • 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.