我的问题是关于InterruptedException
,它是从Thread.sleep
方法抛出的。在使用ExecutorService
时,我注意到一些我不理解的奇怪行为;这是我的意思:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while(true)
{
//DO SOMETHING
Thread.sleep(5000);
}
});
使用此代码,编译器不会给我任何错误或消息,即应该捕获InterruptedException
中的Thread.sleep
。但是,当我尝试更改循环条件并将“ true”替换为这样的变量时:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while(tasksObserving)
{
//DO SOMETHING
Thread.sleep(5000);
}
});
编译器不断抱怨必须InterruptedException
处理。有人可以向我解释为什么会发生这种情况,以及为什么如果将条件设置为true则编译器会忽略InterruptedException?
答案 0 :(得分:56)
之所以这样做,是因为这些调用实际上是对两个不同的重载方法的调用,它们采用两种不同类型的参数,每种类型具有不同的异常处理规范:
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
然后发生的是,编译器将问题的第一种情况下的lambda转换为Callable<?>
功能接口(调用第一个重载方法);在您遇到问题的第二种情况下,将lambda转换为Runnable
功能接口(因此调用了第二个重载方法)。
尽管两个功能接口都没有参数,但是Callable<?>
返回一个值,并且它 throws Exception
(非常重要!):>
- 可致电:
V call() throws Exception;
- 可运行:
public abstract void run();
如果我们切换到将代码修整为相关部分的示例(以便轻松地研究好奇的位),那么我们可以编写与原始示例等效的代码:
ExecutorService executor = Executors.newSingleThreadExecutor();
// LAMBDA COMPILED INTO A 'Callable<?>'
executor.submit(() -> {
while (true)
throw new Exception();
});
// LAMBDA COMPILED INTO A 'Runnable': EXCEPTIONS MUST BE HANDLED BY LAMBDA ITSELF!
executor.submit(() -> {
boolean value = true;
while (value)
throw new Exception();
});
在这些示例中,可能更容易观察到第一个被转换为Callable<?>
而第二个被转换为Runnable
的原因是由于 compiler推论。
在第一种情况下,编译器执行以下操作:
throw new <CHECKED_EXCEPTION>()
的显式调用。Callable<?>
是可用于重载方法的唯一匹配功能接口,因此它将选择它,将lambda转换为Callable<?>
并创建对submit(Callable<?>)
重载方法的调用引用。在第二种情况下,编译器执行以下操作:
Callable<?>
视为lambda的匹配功能接口,因为Callable
确实声明了抛出异常。 (a) Runnable
作为要转换为lambda的其余 fitting 功能接口,并创建对submit(Runnable)
重载方法的调用引用。所有这一切都以委派给用户为代价,处理在{strong> MAY 处抛出的任何Exception
的责任都发生在lambda主体的一部分内。 (a)除了使Callable<?>
为a之外,编译器没有任何理由不默认不将所有lambda都转换为Callable<?>
(并减轻内部复杂性)。比可用的可行替代方案(即Runnable
)更多的限制性功能界面。这种行为符合的原则,即必须严格遵守。但与一个CAN'一样不受限制。
这是一个很好的问题-追逐它,我有很多乐趣,谢谢!
编辑(响应@罗马更正):
我的回答是正确的。编译器之所以决定将问题中的第一个lambda编译为Callable<T>
,而将第二个lambda编译为Runnable
,则是因为其隐式声明例外特征。以下是一个更简单的例子,可以说明这一点:
// LAMBDA COMPILED INTO A 'Callable<?>'
Executors.newSingleThreadExecutor().submit(() -> { throw new Exception(); });
// LAMBDA COMPILED INTO A 'Runnable'
Executors.newSingleThreadExecutor().submit(() -> { });
因此,我正确回答了最初提出的问题-结案了。
现在,从更一般的意义上讲,显然编译器会考虑lambda的 ALL SIGNATURE FEATURES (所有签名特征)来确定将任何lambda编译为(包括参数类型和return类型);但这不是问题所在;如前面的简单示例中清楚显示的那样。
对于示例,您基于整个参数:
Executors.newSingleThreadExecutor().submit(() -> {
while (true) {
Thread.sleep(5000);
}
});
尽管您有索赔...
例如,以下构造将无法编译
编译器无法分辨它是Runnable还是Callable;可以是任何一个。
...此示例确实可以编译。编译器 CAN 告知lambda符合Callable<T>
的签名。实际上,该示例与第一个问题相同,因此我不知道您在说什么。
最后,您随后包含的示例与问题中显示的示例不等效。您添加了return
条语句;这改变了问题,因为它为lambda添加了原始问题中不存在的功能(即lambda的返回类型以区分将其编译为哪个功能接口)并且需要不同的答案;但是再次就没问题了;更糟的是,现在我在问题中看到您的新评论,要求对问题进行修改/编辑以适合您的答案。
答案 1 :(得分:1)
ExecutorService
同时具有submit(Callable)
和submit(Runnable)
方法。
while (true)
),submit(Callable)
和submit(Runnable)
都匹配,因此编译器必须在它们之间进行选择
submit(Callable)
而不是submit(Runnable)
,是因为Callable
比Runnable
更具体 Callable
在throws Exception
中有call()
,因此没有必要在其中捕获异常while (tasksObserving)
)仅submit(Runnable)
匹配,因此编译器选择它
Runnable
的{{1}}方法没有throws
声明,因此,无法捕获run()
方法内部的异常是编译错误。Java语言规范描述了在$15.2.2中的程序编译过程中如何选择方法:
让我们用OP提供的两个代码段中的2个run()
方法来分析情况:
submit()
和
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while(true)
{
//DO SOMETHING
Thread.sleep(5000);
}
});
(其中ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while(tasksObserving)
{
//DO SOMETHING
Thread.sleep(5000);
}
});
不是最终变量)。
首先,编译器必须确定可能适用的方法:$ 15.12.2.1
如果成员是具有固定性n的固定固定性方法,则方法调用的奇偶性等于n,并且对于所有i(1≤i≤n),方法调用的第i个参数为潜在兼容。
在同一部分中进一步
根据以下规则,表达式与目标类型可能兼容:
如果满足以下所有条件,则lambda表达式(第15.27节)可能与功能接口类型(第9.8节)兼容:
目标类型的函数类型的奇偶性与lambda表达式的奇偶性相同。
如果目标类型的函数类型具有void返回值,则lambda主体可以是语句表达式(第14.8节)或与void兼容的块(第15.27.2节)。
如果目标类型的函数类型具有(非无效)返回类型,则lambda主体可以是表达式或值兼容的块(第15.27.2节)。
请注意,在这两种情况下,lambda都是块lambda。
我们还要注意,tasksObserving
的返回类型为Runnable
,因此与void
可能兼容 时,block lambda必须为 void-兼容块。同时,Runnable
具有非无效的返回类型,因此要与Callable
具有可能的兼容性,块lambda必须是值兼容的块< / em>。
$ 15.27.2定义 void-compatible-block 和 value-compatible-block 是什么。
如果块中的每个return语句的格式均为
Callable
,则lambda主体是无效的。如果一个lambda主体不能正常完成(第14.21节)并且该块中的每个return语句的形式为
return;
,则它是值兼容的。
让我们看看$ 14.21,有关return Expression;
循环的段落:
如果满足以下至少一项条件,则while语句可以正常完成:
while语句是可到达的,并且条件表达式不是值为true的常量表达式(第15.28节)。
有一个可达的break语句退出while语句。
在无聊的情况下,lambda实际上是块lambda。
在第一种情况下,可以看到,有一个while
循环,其循环带有一个值为while
的常量表达式(没有true
语句),因此无法正常完成(减$ 14.21);它也没有return语句,因此第一个lambda是 value-compatible 。
同时,根本没有break
语句,因此它也是 void兼容的。因此,最后,在第一种情况下,lambda既是无效的又是值兼容的。
在第二种情况下,从编译器的角度来看,return
循环可以正常完成(因为循环表达式不再是常量表达式),因此,它的全部可以正常完成,因此它不是值兼容的块。但是它仍然是与无效对象兼容的块,因为它不包含任何while
语句。
中间结果是,在第一种情况下,lambda既是 void兼容的块又是 value-compatible块;在第二种情况下,它仅是无效兼容块。
回顾我们前面提到的内容,这意味着在第一种情况下,lambda将与return
和Callable
可能兼容;在第二种情况下,lambda将仅与Runnable
潜在兼容。
对于第一种情况,编译器必须在这两种方法之间进行选择,因为这两种方法均可能可能适用。它使用称为“选择最具体的方法”的过程来进行操作,该过程在$ 15.12.2.5中进行了描述。这是摘录:
如果T不是S的子类型并且以下条件之一为真(其中U1 ... Uk和R1是参数类型,并且S的捕获函数类型的返回类型,而V1 ... Vk和R2是T的函数类型的参数类型和返回类型):
如果e是显式类型的lambda表达式(第15.27.1节),则下列条件之一为真:
R2是无效的。
首先
显式键入带有零参数的lambda表达式。
此外,Runnable
和Runnable
都不是彼此的子类,并且Callable
的返回类型为Runnable
,因此我们有一个匹配项: { {1}}比void
更具体。这意味着在第一种情况下,将在Callable
和Runnable
之间选择带有submit(Callable)
的方法。
对于第二种情况,我们只有一个可能适用的方法submit(Runnable)
,因此选择了它。
因此,最后,我们可以看到在这些情况下,编译器选择了不同的方法。在第一种情况下,lambda推断为在其Callable
方法上具有submit(Runnable)
的{{1}},因此Callable
的调用得以编译。在第二种情况下,throws Exception
没有call()
声明任何可抛出的异常,因此编译器抱怨没有捕获到异常。