Why does a lambda change overloads when it throws a runtime exception?

前端 未结 5 1154
心在旅途
心在旅途 2021-01-31 02:14

Bear with me, the introduction is a bit long-winded but this is an interesting puzzle.

I have this code:

public class Testcase {
    public static void m         


        
相关标签:
5条回答
  • 2021-01-31 02:37

    I wrongly considered this a bug, but it appears to be correct according to §15.27.2. Consider:

    import java.util.function.Supplier;
    
    public class Bug {
        public static void method(Runnable runnable) { }
    
        public static void method(Supplier<Integer> supplier) { }
    
        public static void main(String[] args) {
            method(() -> System.out.println());
            method(() -> { throw new RuntimeException(); });
        }
    }
    
    javac Bug.java
    javap -c Bug
    public static void main(java.lang.String[]);
      Code:
         0: invokedynamic #2,  0      // InvokeDynamic #0:run:()Ljava/lang/Runnable;
         5: invokestatic  #3          // Method add:(Ljava/lang/Runnable;)V
         8: invokedynamic #4,  0      // InvokeDynamic #1:get:()Ljava/util/function/Supplier;
        13: invokestatic  #5          // Method add:(Ljava/util/function/Supplier;)V
        16: return
    

    This happens with jdk-11-ea+24, jdk-10.0.1, and jdk1.8u181.

    zhh's answer led me to find this even simpler test case:

    import java.util.function.Supplier;
    
    public class Simpler {
        public static void main(String[] args) {
            Supplier<Integer> s = () -> { throw new RuntimeException(); };
        }
    }
    

    However, duvduv pointed out §15.27.2, in particular, this rule:

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

    Thus, a block lambda is trivially value-compatible even if it contains no return statement at all. I would have thought, because the compiler needs to infer its type, that it would require at least one return Expression;. Holgar and others have pointed out that this is not necessary with ordinary methods such as:

    int foo() { for(;;); }
    

    But in that case the compiler only needs to ensure there is no return that contradicts the explicit return type; it doesn't need to infer a type. However, the rule in the JLS is written to allow the same freedom with block lambdas as with ordinary methods. Perhaps I should have seen that sooner, but I did not.

    I filed a bug with Oracle but have since sent an update to it referencing §15.27.2 and stating that I believe my original report to be in error.

    0 讨论(0)
  • 2021-01-31 02:44

    The problem is that there are two methods:

    void fun(Runnable r) and void fun(Supplier<Void> s).

    And an expression fun(() -> { throw new RuntimeException(); }).

    Which method will be invoked?

    According to JLS §15.12.2.1, the lambda body is both void-compatible and value-compatible:

    If the function type of T has a void return, then the lambda body is either a statement expression (§14.8) or a void-compatible block (§15.27.2).

    If the function type of T has a (non-void) return type, then the lambda body is either an expression or a value-compatible block (§15.27.2).

    So both methods are applicable to the lambda expression.

    But there are two methods so java compiler needs to find out which method is more specific

    In JLS §15.12.2.5. It says:

    A functional interface type S is more specific than a functional interface type T for an expression e if all of the following are true:

    One of the following is:

    Let RS be the return type of MTS, adapted to the type parameters of MTT, and let RT be the return type of MTT. One of the following must be true:

    One of the following is:

    RT is void.

    So S (i.e. Supplier) is more specific than T (i.e. Runnable) because the return type of the method in Runnable is void.

    So the compiler choose Supplier instead of Runnable.

    0 讨论(0)
  • 2021-01-31 02:54

    First, according to §15.27.2 the expression:

    () -> { throw ... }
    

    Is both void-compatible, and value-compatible, so it's compatible (§15.27.3) with Supplier<CompletionStage<Void>>:

    class Test {
      void foo(Supplier<CompletionStage<Void>> bar) {
        throw new RuntimeException();
      }
      void qux() {
        foo(() -> { throw new IllegalArgumentException(); });
      }
    }
    

    (see that it compiles)

    Second, according to §15.12.2.5 Supplier<T> (where T is a reference type) is more specific than Runnable:

    Let:

    • S := Supplier<T>
    • T := Runnable
    • e := () -> { throw ... }

    So that:

    • MTs := T get() ==> Rs := T
    • MTt := void run() ==> Rt := void

    And:

    • S is not a superinterface or a subinterface of T
    • MTs and MTt have the same type parameters (none)
    • No formal parameters so bullet 3 is also true
    • e is an explicitly typed lambda expression and Rt is void
    0 讨论(0)
  • 2021-01-31 02:59

    It appears that when throwing an Exception, the compiler chooses the interface which returns a reference.

    interface Calls {
        void add(Runnable run);
    
        void add(IntSupplier supplier);
    }
    
    // Ambiguous call
    calls.add(() -> {
            System.out.println("hi");
            throw new IllegalArgumentException();
        });
    

    However

    interface Calls {
        void add(Runnable run);
    
        void add(IntSupplier supplier);
    
        void add(Supplier<Integer> supplier);
    }
    

    complains

    Error:(24, 14) java: reference to add is ambiguous both method add(java.util.function.IntSupplier) in Main.Calls and method add(java.util.function.Supplier) in Main.Calls match

    Lastly

    interface Calls {
        void add(Runnable run);
    
        void add(Supplier<Integer> supplier);
    }
    

    compiles fine.

    So weirdly;

    • void vs int is ambiguous
    • int vs Integer is ambiguous
    • void vs Integer is NOT ambiguous.

    So I figure something is broken here.

    I have sent a bug report to oracle.

    0 讨论(0)
  • 2021-01-31 02:59

    First things first:

    The key point is that overloading methods or constructors with different functional interfaces in the same argument position causes confusion. Therefore, do not overload methods to take different functional interfaces in the same argument position.

    Joshua Bloch, - Effective Java.

    Otherwise, you'll need a cast to indicate the correct overloading:

    queue.add((Runnable) () -> { throw new IllegalArgumentException(); });
                  ^
    

    The same behavior is evident when using an infinite loop instead of a runtime exception:

    queue.add(() -> { for (;;); });
    

    In the cases shown above, the lambda body never completes normally, which adds to the confusion: which overload to choose (void-compatible or value-compatible) if the lambda is implicitly typed? Because in this situation both methods become applicable, for example you can write:

    queue.add((Runnable) () -> { throw new IllegalArgumentException(); });
    
    queue.add((Supplier<CompletionStage<Void>>) () -> {
        throw new IllegalArgumentException();
    });
    
    void add(Runnable task) { ... }
    void add(Supplier<CompletionStage<Void>> task) { ... }
    

    And, like stated in this answer - the most specific method is chosen in case of ambiguity:

    queue.add(() -> { throw new IllegalArgumentException(); });
                           ↓
    void add(Supplier<CompletionStage<Void>> task);
    

    At the same time, when the lambda body completes normally (and is void-compatible only):

    queue.add(() -> { for (int i = 0; i < 2; i++); });
    queue.add(() -> System.out.println());
    

    the method void add(Runnable task) is chosen, because there is no ambiguity in this case.

    As stated in the JLS §15.12.2.1, when a lambda body is both void-compatible and value-compatible, the definition of potential applicability goes beyond a basic arity check to also take into  account  the presence and shape of functional interface target types.

    0 讨论(0)
提交回复
热议问题