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

Gil*_*ili 47 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(); }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我正在尝试将任务添加到队列中并按顺序运行它们.我期待所有3个案例都能调用这个add(Runnable)方法; 然而,实际发生的是案例2被解释为Supplier<CompletionStage<Void>>在返回之前抛出异常,CompletionStage因此"这应该永远不会发生"代码块被触发而案例3永远不会运行.

我确认案例2通过使用调试器逐步调试代码来调用错误的方法.

为什么不Runnable为第二种情况调用该方法?

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

更新:根据JLS§15.12.2.1.确定可能适用的方法,更具体地说是JLS§15.27.2.Lambda Body似乎() -> { throw new RuntimeException(); }属于"无效兼容"和"价值兼容"的范畴.显然,在这种情况下存在一些含糊之处,但我当然不明白为什么SupplierRunnable这里更适合过载.这并不是说前者抛出后者没有的任何例外.

我对规范说不清楚,说明在这种情况下会发生什么.

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

zhh*_*zhh 19

问题是有两种方法:

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

一个表达fun(() -> { throw new RuntimeException(); }).

将调用哪种方法?

根据JLS§15.12.2.1,lambda主体兼容无效且与价值兼容:

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

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

因此两种方法都适用于lambda表达式.

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

JLS§15.12.2.5中.它说:

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

以下之一是:

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

以下之一是:

RT无效.

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

所以编译器选择Supplier而不是Runnable.


duv*_*duv 10

首先,根据§15.27.2表达式:

() -> { throw ... }
Run Code Online (Sandbox Code Playgroud)

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

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

(看它编译)

其次,根据§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
  • MTMTt具有相同的类型参数(无)
  • 没有正式的参数,所以子弹3也是如此
  • e是显式类型的lambda表达式,Rtvoid

  • 我发现你的答案比[zhh's](/sf/answers/3610458911/)更容易理解,但你似乎错过了他提到的一个关键点:§15.12.2.5说Rs比Rt更具体如果Rt是'无效'.因此,"供应商"比"运行"更具体. (2认同)

Pet*_*rey 7

看来,在抛出异常时,编译器会选择返回引用的接口.

interface Calls {
    void add(Runnable run);

    void add(IntSupplier supplier);
}

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

然而

interface Calls {
    void add(Runnable run);

    void add(IntSupplier supplier);

    void add(Supplier<Integer> supplier);
}
Run Code Online (Sandbox Code Playgroud)

抱怨

错误:(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);
}
Run Code Online (Sandbox Code Playgroud)

编译好.

很奇怪;

  • voidvs int是模棱两可的
  • intvs Integer是模棱两可的
  • voidvs Integer不是模棱两可的.

所以我觉得这里有些东西被打破了.

我已经向oracle发送了一个错误报告.


Ole*_*hov 5

首先要做的事情:

关键是在同一参数位置中具有不同功能接口的重载方法或构造函数会导致混淆.因此,不要重载方法在同一参数位置采用不同的功能接口.

Joshua Bloch, - 有效的Java.

否则,您需要一个强制转换来指示正确的重载:

queue.add((Runnable) () -> { throw new IllegalArgumentException(); });
              ^
Run Code Online (Sandbox Code Playgroud)

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

queue.add(() -> { for (;;); });
Run Code Online (Sandbox Code Playgroud)

在上面显示的情况下,lambda主体永远不会正常完成,这增加了混淆:如果lambda被隐式输入,选择哪个重载(void兼容值兼容)?因为在这种情况下两种方法都适用,例如你可以写:

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

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

void add(Runnable task) { ... }
void add(Supplier<CompletionStage<Void>> task) { ... }
Run Code Online (Sandbox Code Playgroud)

并且,如同在这个答案中所述 - 在模糊的情况下选择最具体的方法:

queue.add(() -> { throw new IllegalArgumentException(); });
                       ?
void add(Supplier<CompletionStage<Void>> task);
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

  • @StephanHerrmann并且不管函数接口如何,重载方法都不应该像那个将参数包装成`CompletableFuture.runAsync`或直接排队的问题代码那样做根本不同的事情.举一个肯定的例子,`ExecutorService`是正确的,'submit(Callable)`和`submit(Runnable)`,将函数包装到future中,而`execute(Runnable)`是专用于enqueue的方法`Runnable`直接没有包装.所以调用"错误的"`submit`方法就不会产生负面影响. (3认同)
  • @Holger我同意,严格遵守纪律的危险特征将不那么危险.我仍然认为约书亚布洛赫在引用的建议中非常谦虚.为了更加严重地警告重载,我建议使用https://gbracha.blogspot.com/2009/09/systemic-overload.html - 在Java 8重载可用于非常模糊的编程之前. (3认同)
  • 引用,你的第一件事,应该足以解决这个案子.忽视这个建议是在惹麻烦.即使是专家也难以确定选择哪种过载.@Gili对该计划的读者表示怜悯 - 除非你的目标是为0.1%的前专家创建一个测验. (2认同)
  • @Gili第8章,第52项:明智地使用重载. (2认同)