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

2022-08-31 23:41:14

忍受我,介绍有点冗长,但这是一个有趣的难题。

我有这个代码:

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个案例都调用该方法;但是,实际发生的情况是,情况 2 被解释为在返回 a 之前引发异常的 a,因此触发“这不应该发生”代码块,并且情况 3 永远不会运行。add(Runnable)Supplier<CompletionStage<Void>>CompletionStage

我确认情况 2 通过使用调试器单步执行代码来调用错误的方法。

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

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

更新:根据 JLS §15.12.2.1。确定可能适用的方法,更具体地说是JLS §15.27.2。Lambda Body似乎属于“void-compatible”和“value-compatible”的范畴。很明显,在这种情况下存在一些歧义,但我当然不明白为什么比这里更适合重载。这并不是说前者会引发任何后者没有的异常。() -> { throw new RuntimeException(); }SupplierRunnable

我对规范的了解还不够多,无法说明在这种情况下应该发生什么。

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


答案 1

问题是有两种方法:

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 具有(非 void)返回类型,则 lambda 主体可以是表达式,也可以是值兼容块 (§15.27.2)。

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

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

JLS §15.12.2.5 中。它说:

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

以下是:

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

以下是:

RT 是无效的。

所以 S(即 )比 T(即 )更具体,因为 中方法的返回类型是 。SupplierRunnableRunnablevoid

因此编译器选择而不是 .SupplierRunnable


答案 2

首先,根据 §15.27.2,表达式:

() -> { throw ... }

既兼容又与值兼容,因此它与以下各项兼容 (§15.27.3):voidSupplier<CompletionStage<Void>>

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

(请参阅它编译)

其次,根据§15.12.2.5(其中是引用类型)比以下情况更具体:Supplier<T>TRunnable

让:

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

因此:

  • MT := ==> 卢比 :=T get()T
  • MTt := ==> Rt :=void run()void

和:

  • S不是的超接口或子接口T
  • MTMTt 具有相同的类型参数(无)
  • 没有正式参数,因此项目符号 3 也为真
  • e 是显式类型的 lambda 表达式,Rtvoid

推荐