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(); }属于"无效兼容"和"价值兼容"的范畴.显然,在这种情况下存在一些含糊之处,但我当然不明白为什么Supplier比Runnable这里更适合过载.这并不是说前者抛出后者没有的任何例外.
我对规范说不清楚,说明在这种情况下会发生什么.
我提交了一份错误报告,可在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)更具体,因为方法的返回类型Runnable是void.
所以编译器选择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以下更具体:
让:
Supplier<T>Runnable() -> { throw ... }以便:
T get()==> Rs:=Tvoid run()==> Rt:=void和:
S 不是超接口或子接口 Tvoid看来,在抛出异常时,编译器会选择返回引用的接口.
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发送了一个错误报告.
首先要做的事情:
关键是在同一参数位置中具有不同功能接口的重载方法或构造函数会导致混淆.因此,不要重载方法在同一参数位置采用不同的功能接口.
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检查,也考虑了功能接口目标类型的存在和形状.