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

2024-02-22

请耐心等待,介绍有点冗长,但这是一个有趣的谜题。

我有这个代码:

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或更高版本上,所以一定要在这个环境下测试。

UPDATE: 根据JLS §15.12.2.1。确定潜在适用的方法 https://docs.oracle.com/javase/specs/jls/se10/html/jls-15.html#jls-15.12.2.1更具体地说JLS §15.27.2。拉姆达身体 https://docs.oracle.com/javase/specs/jls/se10/html/jls-15.html#jls-15.27.2看起来() -> { throw new RuntimeException(); }属于“空兼容”和“值兼容”类别。很明显,在这种情况下存在一些含糊之处,但我当然不明白为什么Supplier比过载更合适Runnable这里。并不是说前者抛出了后者不会抛出的任何异常。

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

我提交了一份错误报告,可以在以下位置查看https://bugs.openjdk.java.net/browse/JDK-8208490 https://bugs.openjdk.java.net/browse/JDK-8208490


问题是有两种方法:

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

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

将调用哪个方法?

根据JLS §15.12.2.1 https://docs.oracle.com/javase/specs/jls/se10/html/jls-15.html#jls-15.12.2.1,lambda 主体既兼容 void 又兼容值:

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

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

所以这两种方法都适用于lambda表达式。

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

In JLS §15.12.2.5 https://docs.oracle.com/javase/specs/jls/se10/html/jls-15.html#jls-15.12.2.5。它说:

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

以下之一是:

令RS为MTS的返回类型,适应MTT的类型参数,并令RT为MTT的返回类型。以下其中一项必须为真:

以下之一是:

RT 无效。

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

所以编译器选择Supplier代替Runnable.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么 lambda 在抛出运行时异常时会更改重载? 的相关文章

随机推荐