Thread.sleep 在 lambda 中的无限 while 循环中不需要 'catch (InterruptedException)' - 为什么不呢?

Thread.sleep inside infinite while loop in lambda doesn't require 'catch (InterruptedException)' - why not?

我的问题是关于InterruptedException,这是从Thread.sleep方法抛出的。在使用 ExecutorService 时,我注意到一些我不理解的奇怪行为;这就是我的意思:

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(true)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

使用此代码,编译器不会给我任何错误或消息,表明应该捕获 Thread.sleep 中的 InterruptedException。但是当我试图改变循环条件并用这样的变量替换 "true" 时:

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(tasksObserving)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

编译器不断抱怨必须处理 InterruptedException。有人可以向我解释为什么会发生这种情况,以及为什么如果条件设置为 true 编译器会忽略 InterruptedException?

这样做的原因是,这些调用实际上是对 ExecutorService 中可用的两个不同重载方法的调用;这些方法中的每一个都采用不同类型的单个参数:

  1. <T> Future<T> submit(Callable<T> task);
  2. Future<?> submit(Runnable task);

然后编译器将问题的第一种情况下的 lambda 转换为 Callable<?> 函数接口(调用第一个重载方法);在您的问题的第二种情况下,将 lambda 转换为 Runnable 功能接口(因此调用第二个重载方法),因此需要处理抛出的 Exception ;但在以前的情况下使用 Callable.

尽管两个功能接口都不接受任何参数,Callable<?> return 是一个值:

  1. Callable: V call() throws Exception;
  2. Runnable: public abstract void run();

如果我们切换到 trim 代码到相关部分的示例(以便轻松调查好奇的部分),那么我们可以编写,等同于原始示例:

    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的原因是因为编译器推断

在这两种情况下,lambda 主体都是 void-compatible,因为块中的每个 return 语句都具有 return;.

的形式

现在,在第一种情况下,编译器执行以下操作:

  1. 检测到lambda中的所有执行路径都声明抛出checked exceptions(从现在开始我们将称为'exception',仅表示'checked exceptions')。这包括调用任何声明抛出异常的方法和显式调用 throw new <CHECKED_EXCEPTION>().
  2. 正确得出结论,lambda 的 WHOLE 主体等效于声明抛出异常的代码块;当然 必须 是:已处理或 re-thrown.
  3. 由于 lambda 不处理异常,因此编译器默认假定这些异常必须是 re-thrown.
  4. 安全地推断此 lambda 必须匹配功能接口不能 complete normally,因此是 value-compatible
  5. 由于 Callable<?>Runnable 是此 lambda 的潜在匹配项,编译器会选择最具体的匹配项(以涵盖所有场景);这是 Callable<?>,将 lambda 转换为它的实例并创建对 submit(Callable<?>) 重载方法的调用引用。

而在第二种情况下,编译器执行以下操作:

  1. 检测到lambda中可能存在不要声明抛出异常的执行路径(取决于to-be-evaluated逻辑 ).
  2. 由于并非所有执行路径都声明抛出异常,编译器得出结论认为 lambda 的主体 不一定 等同于声明抛出异常的代码块 - 编译器不会care/pay 注意,如果代码的某些部分确实声明了它们可能,仅当整个主体声明或不声明时。
  3. 安全地推断 lambda 不是 value-compatible;因为它 可能 complete normally.
  4. 选择 Runnable(因为它是唯一可用的 fitting 函数接口,用于将 lambda 转换成)并创建对 [=32= 的调用引用] 重载方法。所有这一切都是以委托给用户为代价的,负责处理任何 抛出的任何 Exception 可能 在 lambda 主体的某些部分发生。

这是一个很好的问题 - 我从中得到了很多乐趣,谢谢!

简要

ExecutorServicesubmit(Callable)submit(Runnable) 方法。

  1. 在第一种情况下(使用 while (true)),submit(Callable)submit(Runnable) 都匹配,因此编译器必须在它们之间进行选择
      选择
    • submit(Callable) 而不是 submit(Runnable) 因为 Callable Runnable
    • 更具体
    • Callablecall()中有throws Exception,所以不需要在里面捕获异常
  2. 第二种情况(带while (tasksObserving))只有submit(Runnable)匹配,所以编译器选择它
    • Runnable 在其 run() 方法上没有 throws 声明,因此未捕获 run() 方法内部的异常是一个编译错误。

完整故事

Java 语言规范描述了在 .2.2 中程序编译期间如何选择方法:

  1. 识别潜在适用的方法 (.12.2.1),分 3 个阶段完成,用于严格、松散和可变元数调用
  2. 从第一步找到的方法中选择最具体的方法 (.12.2.5)。

让我们用OP提供的两个代码片段中的2个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);
        }
    });

(其中 tasksObserving 不是最终变量)。

确定潜在适用的方法

首先,编译器必须确定可能适用的方法:$15.12.2.1

If the member is a fixed arity method with arity n, the arity of the method invocation is equal to n, and for all i (1 ≤ i ≤ n), the i'th argument of the method invocation is potentially compatible, as defined below, with the type of the i'th parameter of the method.

在同一部分更进一步

An expression is potentially compatible with a target type according to the following rules:

A lambda expression (§15.27) is potentially compatible with a functional interface type (§9.8) if all of the following are true:

The arity of the target type's function type is the same as the arity of the lambda expression.

If the target type's function type has a void return, then the lambda body is either a statement expression (§14.8) or a void-compatible block (§15.27.2).

If the target type's function type has a (non-void) return type, then the lambda body is either an expression or a value-compatible block (§15.27.2).

请注意,在这两种情况下,lambda 都是块 lambda。

我们还要注意 Runnable 具有 void return 类型,因此 可能与 Runnable 兼容,一个块 lambda 必须是 void-compatible 块。同时,Callable 有一个 non-void return 类型,所以要 可能与 Callable 兼容 ,块 lambda 必须是 value-compatible 块.

$15.27.2 定义了 void-compatible-blockvalue-compatible-block 是什么。

A block lambda body is void-compatible if every return statement in the block has the form return;.

A block lambda body is value-compatible if it cannot complete normally (§14.21) and every return statement in the block has the form return Expression;.

让我们看看 14.21 美元,关于 while 循环的段落:

A while statement can complete normally iff at least one of the following is true:

The while statement is reachable and the condition expression is not a constant expression (§15.28) with value true.

There is a reachable break statement that exits the while statement.

在 borh 情况下,lambda 实际上是块 lambda。

第一种情况,可以看出,有一个while循环,常量表达式的值为true(没有break语句),所以无法完成通常(减少 14.21 美元);它也没有 return 语句,因此第一个 lambda 是 value-compatible.

同时根本没有return个语句,所以也是void-compatible。所以,最后,在第一种情况下,lambda 既是 void- 又是 value-compatible.

在第二种情况下,while循环从编译器的角度可以正常完成(因为循环表达式不再是常量表达式) ,所以整个lambda可以正常完成,所以它不是一个value-compatible块.但是它仍然是一个void-compatible块因为它不包含return语句。

中间结果是,在第一种情况下,lambda 既是 void-compatible 块 又是 value-compatible 块;在第二种情况下,它是 only a void-compatible block.

回想一下我们之前提到的内容,这意味着在第一种情况下,lambda 将 可能与 CallableRunnable 兼容;在第二种情况下,lambda 只会 可能兼容 Runnable.

选择最具体的方法

对于第一种情况,编译器必须在两种方法之间进行选择,因为这两种方法都可能适用。它使用称为 'Choose the Most Specific Method' 并在 $15.12.2.5 中描述的过程来执行此操作。以下是摘录:

A functional interface type S is more specific than a functional interface type T for an expression e if T is not a subtype of S and one of the following is true (where U1 ... Uk and R1 are the parameter types and return type of the function type of the capture of S, and V1 ... Vk and R2 are the parameter types and return type of the function type of T):

If e is an explicitly typed lambda expression (§15.27.1), then one of the following is true:

R2 is void.

首先,

A lambda expression with zero parameters is explicitly typed.

此外,RunnableCallable都不是彼此的子类,Runnablereturn类型是void,所以我们有一个匹配:CallableRunnable 更具体。这意味着在 submit(Callable)submit(Runnable) 之间,在第一种情况下,将选择 Callable 的方法。

对于第二种情况,我们只有一种可能适用的方法,submit(Runnable),所以选择

那么为什么表面会发生变化呢?

所以,最后,我们可以看到,在这些情况下,编译器选择了不同的方法。在第一种情况下,lambda 被推断为 Callable,其 call() 方法上有 throws Exception,因此 sleep() 调用可以编译。在第二种情况下,Runnable run() 没有声明任何可抛出的异常,因此编译器抱怨未捕获到异常。