为什么Lambda会在引发运行时异常时更改过载?

时间:2018-07-29 04:54:50

标签: java lambda java-8 functional-interface

和我在一起,介绍有些冗长,但这是一个有趣的难题。

我有此代码:

public class Testcase {
    public static void main(String[] args){
        EventQueue queue = new EventQueue();
        queue.add(() -> System.out.println("case1"));
        queue.add(() -> {
            System.out.println("case2");
            throw new IllegalArgumentException("case2-exception");});
        queue.runNextTask();
        queue.add(() -> System.out.println("case3-never-runs"));
    }

    private static class EventQueue {
        private final Queue<Supplier<CompletionStage<Void>>> queue = new ConcurrentLinkedQueue<>();

        public void add(Runnable task) {
            queue.add(() -> CompletableFuture.runAsync(task));
        }

        public void add(Supplier<CompletionStage<Void>> task) {
            queue.add(task);
        }

        public void runNextTask() {
            Supplier<CompletionStage<Void>> task = queue.poll();
            if (task == null)
                return;
            try {
                task.get().
                    whenCompleteAsync((value, exception) -> runNextTask()).
                    exceptionally(exception -> {
                        exception.printStackTrace();
                        return null; });
            }
            catch (Throwable exception) {
                System.err.println("This should never happen...");
                exception.printStackTrace(); }
        }
    }
}

我正在尝试将任务添加到队列中并按顺序运行它们。我期望所有3种情况都调用add(Runnable)方法;但是,实际发生的情况是情况2被解释为Supplier<CompletionStage<Void>>,在返回CompletionStage之前引发了异常,因此触发了“这永远不会发生”代码块,情况3永远不会运行。

我确认情况2通过使用调试器逐步检查代码来调用错误的方法。

为什么第二种情况没有调用Runnable方法?

显然,此问题仅发生在Java 10或更高版本上,因此请确保在此环境下进行测试。

更新:根据JLS §15.12.2.1. Identify Potentially Applicable Methods,更具体地说,JLS §15.27.2. Lambda Body() -> { throw new RuntimeException(); }似乎同时属于“无效兼容”和“价值-兼容”。显然,在这种情况下存在一些歧义,但是我当然不明白为什么Supplier比这里的Runnable更适合重载。前者似乎并没有抛出任何异常。

我对规范不够了解,无法说明这种情况。

我提交了一个错误报告,该报告在https://bugs.openjdk.java.net/browse/JDK-8208490上可见

5 个答案:

答案 0 :(得分:19)

问题在于有两种方法:

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

还有一个表达式fun(() -> { throw new RuntimeException(); })

将调用哪个方法?

根据JLS §15.12.2.1,lambda主体既兼容void又兼容值:

  

如果T的函数类型具有void返回值,则lambda主体可以是语句表达式(第14.8节)或与void兼容的块(第15.27.2节)。

     

如果T的函数类型具有(非无效)返回类型,则lambda主体可以是表达式或值兼容的块(第15.27.2节)。

因此,这两种方法都适用于lambda表达式。

但是有两种方法,因此Java编译器需要找出哪种方法更具体

JLS §15.12.2.5中。它说:

  

如果满足以下所有条件,则对于表达式e,功能接口类型S比功能接口类型T更具体:

以下之一是:

  

让RS为MTS的返回类型,以适应MTT的类型参数,让RT为MTT的返回类型。以下条件之一必须为真:

以下之一是:

  

RT是无效的。

所以S(即Supplier)比T(即Runnable)更具体,因为Runnable中方法的返回类型为void

因此,编译器选择Supplier而不是Runnable

答案 1 :(得分:10)

首先,根据§15.27.2表达式:

() -> { throw ... }

既与void兼容,又与值兼容,因此与Supplier<CompletionStage<Void>>兼容(§15.27.3):

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

(请参见编译)

第二,根据§15.12.2.5 Supplier<T>(其中T是引用类型)比Runnable更具体:

让:

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

因此:

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

并且:

  • S不是T的超级接口或子接口
  • MTs MTt 具有相同的类型参数(无)
  • 没有正式参数,因此项目符号3也是正确的
  • e 是显式类型的lambda表达式, Rt void

答案 2 :(得分:7)

似乎抛出异常时,编译器选择返回引用的接口。

interface Calls {
    void add(Runnable run);

    void add(IntSupplier supplier);
}

// Ambiguous call
calls.add(() -> {
        System.out.println("hi");
        throw new IllegalArgumentException();
    });

但是

interface Calls {
    void add(Runnable run);

    void add(IntSupplier supplier);

    void add(Supplier<Integer> supplier);
}

抱怨

  

错误:(24,14)java:要添加的引用不明确     Main.Calls中的方法add(java.util.function.IntSupplier)和Main.Calls中的方法add(java.util.function.Supplier)都匹配

最后

interface Calls {
    void add(Runnable run);

    void add(Supplier<Integer> supplier);
}

编译良好。

很奇怪;

  • voidint模棱两可
  • intInteger模棱两可
  • voidInteger并不确定。

所以我觉得这里有些破损。

我已将错误报告发送给oracle。

答案 3 :(得分:5)

第一件事:

  

关键是使用以下方法重载方法或构造函数   同一自变量位置中的不同功能接口导致   混乱。 因此,请勿重载方法以采取不同的方法   功能接口处于相同的参数位置。

Joshua Bloch,-有效的Java。

否则,您将需要强制转换以指示正确的重载:

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

使用无限循环而不是运行时异常时,相同的行为显而易见:

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

在上述情况下,lambda主体从未正常完成,这增加了混乱:如果选择哪种重载( void-compatible value-compatible ) lambda是隐式键入的吗?因为在这种情况下这两种方法都适用,例如,您可以编写:

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

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

void add(Runnable task) { ... }
void add(Supplier<CompletionStage<Void>> task) { ... }

并且,如本answer中所述,在出现歧义的情况下,选择最具体的方法:

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

同时,当lambda主体正常完成时(并且仅与void兼容):

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

选择方法void add(Runnable task),因为在这种情况下没有歧义。

JLS §15.12.2.1中所述,当lambda主体既兼容和值兼容时,潜在适用性的定义也超出了基本仔细检查也要考虑功能接口目标类型的存在和形状。

答案 4 :(得分:2)

我错误地认为这是一个错误,但是根据§15.27.2,它似乎是正确的。考虑:

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

这发生在jdk-11-ea + 24,jdk-10.0.1和jdk1.8u181。

zhh的答案使我找到了这个更简单的测试用例:

import java.util.function.Supplier;

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

但是,duvduv指出§15.27.2,尤其是该规则:

  

如果lambda主体无法正常完成(第14.21节),并且该区块中的每个return语句的格式均为return Expression ;。

因此,即使lambda根本不包含任何return语句,它也是很小的值兼容的。我本来以为,因为编译器需要推断其类型,所以它至少需要一个返回 Expression ;。 Holgar等人指出,使用普通方法(例如:

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

但是在那种情况下,编译器只需要确保没有与显式返回类型相矛盾的返回即可。它不需要推断类型。但是,JLS中的规则被编写为允许使用块lambda与常规方法一样的自由度。也许我早该看到,但我没有。

我提交了a bug with Oracle,但此后发送了更新,引用了第15.27.2节的内容,并指出我认为我的原始报告有误。