和我在一起,介绍有些冗长,但这是一个有趣的难题。
我有此代码:
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上可见
答案 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
更具体:
让:
Supplier<T>
Runnable
() -> { throw ... }
因此:
T get()
==> Rs := T
void run()
==> Rt := void
并且:
S
不是T
的超级接口或子接口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);
}
编译良好。
很奇怪;
void
与int
模棱两可int
与Integer
模棱两可void
与Integer
并不确定。所以我觉得这里有些破损。
我已将错误报告发送给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节的内容,并指出我认为我的原始报告有误。