为啥在这种情况下允许抛出已检查的异常类型?

Posted

技术标签:

【中文标题】为啥在这种情况下允许抛出已检查的异常类型?【英文标题】:Why is throwing a checked exception type allowed in this case?为什么在这种情况下允许抛出已检查的异常类型? 【发布时间】:2014-09-18 20:14:21 【问题描述】:

我偶然注意到这个throw 语句(从一些更复杂的代码中提取)可以编译:

void foo() 
    try 

     catch (Throwable t) 
        throw t;
    

有一个短暂而快乐的时刻,我以为受检异常终于决定死了,但它仍然对此感到高兴:

void foo() 
    try 

     catch (Throwable t) 
        Throwable t1 = t;
        throw t1;
    

try 块不必为空;只要该代码不引发检查异常,它似乎就可以有代码。这似乎是合理的,但我的问题是,语言规范中的什么规则描述了这种行为?据我所知,§14.18 The throw Statement 明确禁止它,因为t 表达式的类型是一个检查异常,它没有被捕获或声明被抛出。 (?)

【问题讨论】:

+1。这实际上引发了关于 multicatch catch (final RuntimeException | Error e) throw e; 的相同问题,因为根据 §14.20,e 的类型是 Throwable 【参考方案1】:

这是因为在 Java 7 中引入的 Project Coin 中包含的更改允许通过重新抛出原始异常进行一般异常处理。这是一个适用于 Java 7 但不适用于 Java 6 的示例:

public static demoRethrow() throws IOException 
    try 
        throw new IOException("Error");
    
    catch(Exception exception) 
        /*
         * Do some handling and then rethrow.
         */
        throw exception;
    

您可以阅读整篇文章来解释更改here。

【讨论】:

这似乎是它,但它在 langspec 中的哪里? 14.18 说“以下三个条件中的至少一个必须为真,否则会发生编译时错误”。这不是三个条件之一。 没错。并且由于 Java 7 中引入的增强分析,可以确定第三个条件(throw 语句包含在方法或构造函数声明中,并且表达式的类型可分配(第 5.2 节)到至少一种类型声明的 throws 子句(第 8.4.6 节、第 8.8.5 节)中列出的。)为真。 但是表达式类型是 Throwable 并且不能“分配”给 RuntimeExceptionError,因为这需要强制转换,这似乎是 §5.5,而不是 §5.2 . @Boann 这是真的。但是,编译器现在可以检测到这个Throwable 必须是RuntimeExceptionError,因为在try 块中不能抛出Throwable 的其他子类型。 如果 throwable 可能是一个检查异常(因为这样的异常可能会在try-block 中抛出),那么它会抱怨这个检查异常被抛出(正如你在问题)。【参考方案2】:

我认为您提到的 §14.18 The throw Statement 中的措辞是 JLS 中的一个错误——本应使用 Java SE 7 更新的文本,但没有。

描述预期行为的 JLS 文本位在 §11.2.2 Exception Analysis of Statements:

如果 throw 语句抛出的表达式是 catch 子句 C 的最终或有效最终异常参数,则可以抛出异常类 E iff:

E是声明C的try语句的try块可以抛出的异常类;和 E 是与任何 C 的可捕获异常类兼容的赋值;和 E 与在同一 try 语句中声明在 C 左侧的 catch 子句的任何可捕获异常类的赋值都不兼容。

第一个要点是相关的;因为catch-clause 参数t 实际上是最终的(意味着它永远不会分配或递增或递减;参见§4.12.4 final Variables),throw t 只能抛出 try 块可以抛出的东西。

但正如您所说,第 14.18 节中的编译时检查并没有对此做任何考虑。 §11.2.2 不决定什么是允许的,什么是不允许的;相反,它应该是对可以抛出的各种限制的后果的分析。 (这种分析确实反馈到规范的更规范的部分——第 14.18 节本身在其第二个要点中使用它——但第 14.18 节不能只说“如果它抛出一个它不能抛出的异常,这是一个编译时错误按照 §11.2.2" 抛出,因为那将是循环的。)

所以我认为需要调整 §14.18 以适应 §11.2.2 的意图。

很好的发现!

【讨论】:

§14.18 还说“throw 语句可以抛出的异常类型在 §11.2.2 中指定”。这句话前面的要点对此有点措辞不当。 @ntoskrnl 这似乎有点用词错误。我完全错过了 11.2.2。 与旧的 JLS 相比,§14.18 中的“三个条件”基本没有变化,但§11.2.2 发生了巨大变化。有趣的。但这肯定是正确的部分,因为如果我在throw t; 之前插入t = null;,那么它不再是“有效的最终”,它现在会抱怨未声明的异常。 (顺便说一句,接受您的回答,因为它是最详细的。谢谢。) @ntoskrnl:是的——但这并不能取代条件列表。 (注意,第 11.2.2 节中的“can throw”是指“可能会抛出”,而不是“允许抛出”。)这并不是说第三个条件是“措辞有点差”;它实际上措辞很清楚。 (更重要的是,我认为它实际上很好。应该改变的是,应该有第四个条件允许第 11.2.2 条暗示的行为应该被允许。)【参考方案3】:

此行为在11.2. Compile-Time Checking of Exceptions 中的 JLS 中有详细描述:

一个throw 语句,其抛出的表达式是最终的或有效的 catch 子句 C 的最终异常参数可以抛出异常 E 类当且仅当:

E 是一个异常类,它表示 try 语句的 try 块 声明 C 可以抛出;和

E 是与 C 的任何可捕获异常兼容的赋值 课程;和

E 的赋值与任何可捕获的异常都不兼容 在同一 try 中声明在 C 左侧的 catch 子句的类 声明。

(强调我的。)

您的第二个示例失败,因为 t1 不是“catch 子句的异常参数”。

【讨论】:

以上是关于为啥在这种情况下允许抛出已检查的异常类型?的主要内容,如果未能解决你的问题,请参考以下文章

为啥不抛出异常的代码允许捕获已检查的异常?

JLS 的哪些部分证明能够像未经检查一样抛出已检查异常?

AppDomain.UnhandledException 自动重新抛出已处理的异常

Spring Security 部署失败:生命周期方法 [initialize] 不得抛出已检查异常

终端操作(例如 forEach)可以重新抛出已检查的异常吗?

Java异常