简述
ExecutorService
接口有两个方法:submit(Callable)
和submit(Runnable)
。
- 在第一个例子中(包含
while(true)
的代码段),submit(Callable)
和submit(Runnable)
都匹配,编译器需要在两者之间做出选择
- 由于
Callable
比Runnable
更具体,因此编译器选择了submit(Callable)
Callable
的call()
方法声明了throws Exception
,因此不需要在其中捕获异常
- 在第二个例子中(包含
while (tasksObserving)
的代码段),只有submit(Runnable)
与之匹配,所以编译器选择了它
Runnable
的run()
方法没有throws
声明,因此如果不在run()
内部捕获异常会导致编译错误
详细介绍
Java语言规范描述了程序编译期间如何选择方法:$15.2.2 :
- 识别可能适用的方法($15.12.2.1),对于精确匹配、宽松匹配和可变参数匹配,分3个阶段来完成
- 从第一步找到的方法中选择最具体的方法($15.12.2.5)。
现在让我们分析OP提供的两个代码片段中的两个submit()
方法的情况:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while(true)
{
Thread.sleep(5000);
}
});
并且
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while(tasksObserving)
{
Thread.sleep(5000);
}
});
(其中tasksObserving
不是最终变量)。
识别潜在适用的方法
首先,编译器必须识别出潜在适用的方法:$15.12.2.1
如果成员是具有n个参数的固定参数数量的方法,那么方法调用的参数数量等于n,并且对于所有i(1≤i≤n),方法调用的第i个参数与该方法的第i个参数的类型是潜在兼容的,下面定义了这一概念。
在同一段落稍后的地方:
根据以下规则,一个表达式可以与目标类型潜在兼容:
如果函数接口类型为void,则lambda表达式(§15.27)可以与其潜在兼容, 如果以下所有条件均为真:
目标类型函数类型的元数与lambda表达式相同。
如果目标类型的函数类型具有void返回值,则lambda主体是语句表达式(§14.8)或void兼容块(§15.27.2)之一。
如果目标类型的函数类型具有非void返回类型,则lambda主体是表达式或值兼容块(§15.27.2)之一。
请注意,在这两种情况下,lambda都是块lambda。
还要注意,Runnable
具有void
返回类型,因此,为了与Runnable
潜在兼容,块lambda必须是void兼容块。同时,Callable
具有非void返回类型,因此,块lambda为了与Callable
潜在兼容,必须是值兼容块。
$15.27.2定义了void兼容块和值兼容块的概念。
如果块lambda中的每个return语句的形式为return;
,则块lambda主体是void兼容的。
如果块lambda无法正常完成(§14.21),并且块中的每个return语句的形式为return Expression;
,则块lambda主体是值兼容的。
让我们看看$14.21中关于while
循环的段落:
当且仅当以下至少有一个条件为真时,while语句可以正常完成:
while语句是可达的,并且条件表达式不是值为true的常量表达式(§15.28)。
存在可达的break语句退出while语句。
在这两种情况下,lambda实际上都是块lambda。
在第一种情况下,可以看到有一个带有常量表达式的
while
循环,其值为
true
(没有
break
语句),因此它无法正常完成(见$14.21);同时它没有返回语句,因此第一个lambda是
value-compatible。
同时,根本没有
return
语句,因此它也是
void-compatible。因此,
在第一种情况下,lambda既是void-compatible,也是value-compatible。
在第二种情况下,
while
循环
从编译器的角度来看可以正常完成(因为循环表达式不再是常量表达式),因此整个lambda
可以正常完成,因此它
不是value-compatible block。但是,因为它不包含任何
return
语句,所以它仍然是
void-compatible block。
中间结果是,在第一种情况下,lambda既是
void-compatible block,也是
value-compatible block;而在第二种情况下,它只是
void-compatible block。
回想一下我们之前注意到的,这意味着在第一种情况下,lambda将与
Callable
和
Runnable
都是
potentially compatible;而在第二种情况下,lambda只与
Runnable
是
potentially compatible。
选择最具体的方法
对于第一种情况,编译器必须在两个方法之间进行选择,因为两者都是
potentially applicable。它使用称为“选择最具体方法”的过程来进行选择,并在$15.12.2.5中描述。以下是摘录:
如果T不是S的子类型并且以下条件之一成立,则函数式接口类型S对于表达式e比函数式接口类型T更具体(其中U1 ... Uk和R1是S的捕获函数类型的参数类型和返回类型,V1 ... Vk和R2是T的函数类型的参数类型和返回类型):
如果e是显式类型的lambda表达式(§15.27.1),则以下条件之一成立:
R2是void。
首先,
带有零个参数的lambda表达式是显式类型的。
此外,
Runnable
和
Callable
都不是彼此的子类,而且
Runnable
的返回类型为
void
,因此我们有一个匹配项:
Callable
比Runnable
更具体。这意味着在
submit(Callable)
和
submit(Runnable)
之间,在第一个情况下将选择带有
Callable
的方法。
至于第二种情况,我们只有一个
可能适用的方法
submit(Runnable)
,所以它被选中。
那么为什么会出现更改?
因此,最终我们可以看到编译器在这些情况下选择了不同的方法。在第一个情况下,Lambda被推断为具有在其
call()
方法上带有
throws Exception
的
Callable
,因此
sleep()
调用编译。在第二种情况下,它是没有声明任何可抛出异常的
run()
的
Runnable
,因此编译器会抱怨异常未被捕获。
DO SOMETHING
的细节吗? 给出精确的答案非常重要。例如,如果DO SOMETHING
为空,那么带有while (true)
的第一个代码块将无法编译通过。有关更多详细信息,请参阅我的答案。 - Roman Puchkovskiy