java lambda中无限while循环内的线程睡眠不需要'catch(InterruptedException)' -为什么不呢?

5jdjgkvh  于 2023-02-02  发布在  Java
关注(0)|答案(2)|浏览(153)

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

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

在这段代码中,编译器不会给出任何错误信息,也不会告诉我应该捕捉来自Thread.sleepInterruptedException,但是当我试图改变循环条件,用下面这样的变量替换“true”时:

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

编译器不断地抱怨必须处理InterruptedException。有人能给我解释一下为什么会发生这种情况吗?为什么如果条件设置为true,编译器会忽略InterruptedException?

u3r8eeie

u3r8eeie1#

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

  1. <T> Future<T> submit(Callable<T> task); 2. Future<?> submit(Runnable task);
    然后,编译器将第一个问题中的lambda转换为Callable<?>函数接口(调用第一个重载的方法);在第二种情况下,将lambda转换为Runnable函数接口(因此调用第二个重载方法),因此需要处理抛出的Exception;但在先前使用X1 M6 N1 X的情况下不是这样。
    尽管两个函数接口都不接受任何参数,但Callable<?>返回一个值
    1.可调用〈?〉:V call() throws Exception; 2.可运行: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的原因是由于 * 编译器推断 *。
在这两种情况下,lambda主体都是void兼容的,因为块中的每个return语句都具有return;的形式。
现在,在第一种情况下,编译器执行以下操作:
1.检测lambda中的所有执行路径都声明引发checked exceptions(从现在起,我们将称为 “异常”,仅表示 “已检查异常”)。这包括调用任何声明引发异常的方法以及显式调用throw new <CHECKED_EXCEPTION>()
1.正确地得出结论,lambda的WHOLE主体等效于声明引发异常的代码块;当然,必须为:处理或重新投掷。
1.因为lambda不处理异常,那么编译器默认假设这些异常必须被重新抛出。
1.安全地推断此lambda必须匹配函数接口,而函数接口不能complete normally,因此是值兼容的。
1.由于Callable<?>Runnable是这个lambda的潜在匹配,编译器选择最具体的匹配(以覆盖所有场景);即Callable<?>,将lambda转换为它的示例,并创建对submit(Callable<?>)重载方法的调用引用。
而在第二种情况下,编译器执行以下操作:
1.检测lambda中可能存在DO NOT声明引发异常的执行路径(取决于 * 要计算的逻辑 *)。
1.由于并非所有执行路径都声明抛出异常,编译器得出结论,lambda的主体不必等同于声明抛出异常的代码块-编译器不关心/注意代码的某些部分是否声明它们可能,只有整个主体是否声明。
1.安全地推断lambda与值不兼容;因为它是五月complete normally
1.选择Runnable(因为它是lambda要转换到的唯一可用的 fitting 函数接口)并创建对submit(Runnable)重载方法的调用引用。所有这些都是以委托给用户为代价的,委托用户处理任何抛出的Exception,无论它们可能出现在lambda主体的任何部分。

z0qdvdin

z0qdvdin2#

简单来说

ExecutorService同时具有submit(Callable)submit(Runnable)方法。

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

∮完整的故事∮
Java语言规范在$15.2.2中描述了如何在程序编译期间选择方法:
1.识别可能适用的方法($15.12.2.1),分为3个阶段,分别用于严格、宽松和可变arity调用
1.从第一步找到的方法中选择最具体的方法(15.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不是最终变量)。

确定潜在适用方法

首先,编译器必须识别 * 可能适用的方法 *:十五元一二角二分一
如果成员是一个固定arity方法,arity为n,则方法调用的arity等于n,并且对于所有i(1 ≤ i ≤ n),方法调用的第i个参数与方法的第i个参数的类型 * 潜在兼容 *,如下所定义。
在同一部分再往前一点
根据以下规则,表达式与目标类型 * 可能兼容 *:
如果满足以下所有条件,则lambda表达式(参见15.27节)可能与函数接口类型(参见9.8节)兼容:
目标类型的函数类型的arity与lambda表达式的arity相同。
如果目标类型的函数类型有一个void返回,那么lambda主体要么是一个语句表达式(参见14.8节),要么是一个void兼容块(参见15.27.2节)。
如果目标类型的函数类型有一个(非空的)返回类型,那么lambda主体要么是一个表达式,要么是一个值兼容的块(参见15.27.2节)。
让我们注意在这两种情况下,lambda都是块lambda。
我们还要注意Runnablevoid返回类型,所以要与Runnable * 潜在兼容 *,块lambda必须是 * void兼容块 *。同时,Callable有非void返回类型,所以要与Callable * 潜在兼容 *,块lambda必须是 * value兼容块 *。
$15.27.2定义了什么是 * 空兼容块 * 和 * 值兼容块 *。
如果块中的每个return语句都具有return;,则块lambda主体是void兼容的。
块lambda体是值兼容的,如果它不能正常完成(参见14.21节),并且块中的每个return语句的形式都是return Expression;
让我们看一下$14.21,关于while循环的段落:
while语句可以正常完成的条件是至少满足以下条件之一:
while语句是可达的,条件表达式不是值为true的常量表达式(参见15.28节)。
存在退出while语句的可访问break语句。
在borh的情况下,lambda实际上是块lambda。
在第一种情况下,如图所示,存在一个while循环,其中常量表达式的值为true(没有break语句),因此它无法正常完成(相差$14.21);而且,它没有返回语句,因此第一个lambda是 * 值兼容的 *。
同时,根本没有return语句,所以它也是 * void兼容的 *,所以,最后,在第一种情况下,lambda是void和value兼容的
在第二种情况下,从编译器的Angular 来看,while循环 * 可以正常地完成 *(因为循环表达式不再是常量表达式),所以整个lambda * 可以正常地完成 *,所以它不是一个 * 值兼容的块 *,但是它仍然是一个void兼容的块,因为它不包含return语句。
中间结果是,在第一种情况下,lambda既是 * 空兼容块 * 又是 * 值兼容块 *;在第二种情况下,它是唯一的空兼容块 *。
回想一下我们前面提到的,这意味着在第一种情况下,lambda将与CallableRunnable都是 * 潜在兼容的 *;在第二种情况下,λ将仅与X1 M37 N1 X“潜在兼容”。

选择最具体的方法

对于第一种情况,编译器必须在这两种方法之间做出选择,因为它们都 * 可能适用 *。它使用$15.12.2.5中描述的名为'Choose the Most Specific Method'的过程来完成。

对于表达式e,如果T不是S的子类型并且以下之一为真,则函数接口类型S比函数接口类型T更具体(其中U1...Uk和R1是S的捕获的函数类型的参数类型和返回类型,并且V1...Vk和R2是T的函数类型的参数类型和返回类型):
如果e是一个显式类型的lambda表达式(参见15.27.1节),那么以下条件之一成立:
R2无效。
首先,
具有零个参数的lambda表达式是显式类型的。
另外,RunnableCallable都不是彼此的子类,并且Runnable返回类型是void,所以我们有一个匹配:* * CallableRunnable更具体**。这意味着在第一种情况下,在submit(Callable)submit(Runnable)之间将选择使用Callable的方法。
对于第二种情况,我们只有一个 * 潜在适用的 * 方法submit(Runnable),因此选择它。

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

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

相关问题