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

Posted

技术标签:

【中文标题】JLS 的哪些部分证明能够像未经检查一样抛出已检查异常?【英文标题】:What parts of the JLS justify being able to throw checked exceptions as if they were unchecked? 【发布时间】:2012-09-16 19:57:38 【问题描述】:

我有recently discovered and blogged about the fact,可以通过 javac 编译器偷偷检查异常并将其抛出到不应抛出的地方。这在 Java 6 和 7 中编译和运行,抛出一个 SQLException 没有 throwscatch 子句:

public class Test 

    // No throws clause here
    public static void main(String[] args) 
        doThrow(new SQLException());
    

    static void doThrow(Exception e) 
        Test.<RuntimeException> doThrow0(e);
    

    static <E extends Exception> void doThrow0(Exception e) throws E 
        throw (E) e;
    

生成的字节码表明 JVM 并不真正关心已检查/未检查的异常:

// Method descriptor #22 (Ljava/lang/Exception;)V
// Stack: 1, Locals: 1
static void doThrow(java.lang.Exception e);
  0  aload_0 [e]
  1  invokestatic Test.doThrow0(java.lang.Exception) : void [25]
  4  return
    Line numbers:
      [pc: 0, line: 11]
      [pc: 4, line: 12]
    Local variable table:
      [pc: 0, pc: 5] local: e index: 0 type: java.lang.Exception

// Method descriptor #22 (Ljava/lang/Exception;)V
// Signature: <E:Ljava/lang/Exception;>(Ljava/lang/Exception;)V^TE;
// Stack: 1, Locals: 1
static void doThrow0(java.lang.Exception e) throws java.lang.Exception;
  0  aload_0 [e]
  1  athrow
    Line numbers:
      [pc: 0, line: 16]
    Local variable table:
      [pc: 0, pc: 2] local: e index: 0 type: java.lang.Exception

JVM 接受这是一回事。但我怀疑 Java-the-language 是否应该这样做。 JLS 的哪些部分证明了这种行为?它是一个错误吗?还是 Java 语言的一个隐藏得很好的“特性”?

我的感受是:

doThrow0()&lt;E&gt; 绑定到doThrow() 中的RuntimeException。因此,在doThrow() 中不需要遵循JLS §11.2 的throws 子句。 RuntimeExceptionException 赋值兼容,因此编译器不会生成强制转换(这将导致 ClassCastException)。

【问题讨论】:

因为RuntimeException extends Exception 这是可能的。但这是允许的。 JVM 不在乎,这是肯定的。这只是一个编译时验证。我喜欢这个,也许我可以对其进行调整,最终能够避免所有那些愚蠢的 try-catch e-throw new RuntimeException(e)! 老兄,这是在 SO 在您的博客上获得声誉的好方法;) 我真的很喜欢它。它给了这个问题更多的背景。垃圾邮件是一个如此强烈的词。 【参考方案1】:

所有这些都等于利用了未经检查的转换为泛型类型不是编译器错误的漏洞。如果您的代码包含这样的表达式,则它被明确地设为类型不安全。而且由于检查异常的检查严格来说是一个编译时过程,所以运行时不会有任何中断。

Generics 的作者的回答很可能是“如果你使用未经检查的强制转换,那是你的问题”。

我在您的发现中看到了一些非常积极的东西——突破了检查异常的堡垒。不幸的是,这并不能将现有的受检查异常中毒的 API 变成更好用的东西。

这有什么帮助

在我的一个典型的分层应用项目中,会有很多这样的样板:

try 
  ... business logic stuff ...
 
catch (RuntimeException e)  throw e;  
catch (Exception e)  throw new RuntimeException(e); 

我为什么要这样做?很简单:没有要捕获的业务价值异常;任何异常都是运行时错误的症状。异常必须沿调用堆栈向上传播到全局异常屏障。有了 Lukas 的出色贡献,我现在可以写作了

try 
  ... business logic stuff ... 
 catch (Exception e)  throwUnchecked(e); 

这可能看起来不多,但在整个项目中重复 100 次后,收益就会累积。

免责声明

在我的项目中,有关于异常的严格纪律,所以这非常适合他们。 这种诡计不能作为一般编码原则采用。在许多情况下,包装异常仍然是唯一安全的选择。

【讨论】:

"已检查异常中毒的 API" ;-) 这可以彻底破坏希望声明检查异常的调用者。想象一个包含多个调用的 try 块。一种称为声明throws FooException 的方法,另一种没有但无论如何都会抛出它。处理 FooException 的 catch 块可能不会期望程序在通过时所处的状态。 @Ben 我同意,这绝对不能被视为一般建议。首先,它仅在您编写引发异常的代码时才有用,在这种情况下,您应该选择一个未经检查的异常来抛出。 @MarkoTopolnik:附带说明一下,堆栈跟踪不会改进,因为new SQLException() 的位置保持不变。实际throw 语句的位置与堆栈跟踪无关。 @Saintali 我会绝对、肯定、永远、永远在公共图书馆使用这些技巧!但是让我们登陆地球并承认没有人编写公共库,除了少数几个,即使对他们来说,这也是他们代码的 10%。我可以在免责声明中写一些类似的东西,但是我的用例规范已经很好地涵盖了基础,我会说【参考方案2】:

此示例记录在 The CERT Oracle Secure Coding Standard for Java 中,其中记录了几个不合规的代码示例。

不合规代码示例(通用异常)

具有参数化异常声明的泛型类型的未经检查的强制转换也可能导致意外的检查异常。除非警告被禁止,否则编译器会诊断所有此类强制转换。

interface Thr<EXC extends Exception> 
  void fn() throws EXC;


public class UndeclaredGen 
static void undeclaredThrow() throws RuntimeException 
@SuppressWarnings("unchecked")  // Suppresses warnings
Thr<RuntimeException> thr = (Thr<RuntimeException>)(Thr)
  new Thr<IOException>() 
    public void fn() throws IOException 
      throw new IOException();

  ;
thr.fn();


public static void main(String[] args) 
  undeclaredThrow();
 

这是可行的,因为RuntimeExceptionException 的子类,并且您不能将任何从Exception 扩展为RuntimeException 的类转换,但如果您像下面这样进行转换,它将起作用

 Exception e = new IOException();
 throw (RuntimeException) (e);

您正在做的情况与此相同。 因为这是显式类型转换,所以此调用将导致 ClassCastException 但是编译器允许它。

由于 Erasure 但是在您的情况下没有涉及演员表,因此在您的情况下它不会抛出 ClassCastException

在这种情况下,编译器无法限制您正在进行的类型转换。

但是,如果您将方法签名更改为以下,它将开始抱怨它。

static <E extends Exception> void doThrow0(E e) throws E 

【讨论】:

你的直觉和我的一样。请注意,doThrow0() 的目的是在 doThrow() 中隐藏从 ExceptionE 的不安全转换...因此,为了我的示例,在这里更改签名没有意义。 @LukasEder 它也可以在没有泛型的情况下工作。请检查示例。 但是 AmitD,您的代码抛出 ClassCastException,掩盖您想要抛出的异常! @MarkoTopolnik ClassCastException 是运行时行为,但编译时行为是相同的。由于 Erasure,两者之间存在差异。 @LukasEder 检查链接以预订这可能是您问题的答案。【参考方案3】:

嗯,这是引发检查异常的多种方法之一,就好像它没有被检查一样。 Class.newInstance() 是另一个,Thread.stop(Trowable) 已弃用。

JLS 不接受此行为的唯一方法是运行时 (JVM) 是否会强制执行。

至于它是否被指定:它不是。已检查和未检查的异常表现相同。检查异常只需要一个 catch 块或 throws 子句。

编辑:基于 cmets 中的讨论,一个基于列表的示例揭示根本原因:擦除

public class Main 
    public static void main(String[] args) 
        List<Exception> myCheckedExceptions = new ArrayList<Exception>();
        myCheckedExceptions.add(new IOException());

        // here we're tricking the compiler
        @SuppressWarnings("unchecked")
        List<RuntimeException> myUncheckedExceptions = (List<RuntimeException>) (Object) myCheckedExceptions;

        // here we aren't any more, at least not beyond the fact that type arguments are erased
        throw throwAny(myUncheckedExceptions);
    

    public static <T extends Throwable> T throwAny(Collection<T> throwables) throws T 
        // here the compiler will insert an implicit cast to T (just like your explicit one)
        // however, since T is a type variable, it gets erased to the erasure of its bound
        // and that happens to be Throwable, so a useless cast...
        throw throwables.iterator().next();
    

【讨论】:

Class.newInstance()Thread.stop(Throwable) 最终从 native 实现中抛出异常。所以 JLS 可能没问题。即使它们 表现 相同(JVM 不关心),对于 JLS,它们也不相同(javac 关心),正如在各个章节中可以看到的那样,例如 §11.2 /跨度> @LukasEder 异常是类型系统的一部分。如果你抛弃(不是双关语)类型信息,编译器就没有什么可处理的了。您确信那里没有已检查的异常。我可能会补充说,您这样做时使用了未经检查的演员表,这会引发警告,所以我不确定您还要求什么。 “抛弃” ;-)。是的,这就是我要找的。真的是这样,我可以合法地说服编译器不涉及检查异常吗?我想我可以,因为上面提到的原因。但我对此感到惊讶,所以我想问这个问题。 @LukasEder 是的,是演员阵容。一种更复杂的方法是将其填充到List&lt;Exception&gt; 并将其转换为List&lt;RuntimeException&gt;。然后,您可以将其拉回并作为未经检查的异常抛出。是擦除。再次。 :D 列表的好主意!不幸的是,当拉出RuntimeException 时,这似乎会导致ClassCastException ...【参考方案4】:

JLS 11.2:

对于每个可能结果的检查异常,抛出 方法(第 8.4.6 节)或构造函数(第 8.8.5 节)的子句必须提及 该异常的类或该类的超类之一 该例外(第 11.2.3 节)。

这清楚地表明 doThrow 的 throws 子句中必须有 Exception。或者,由于涉及向下转换(对 RuntimeException 的异常),因此必须检查 Exception IS RuntimeException,这在示例中应该失败,因为正在转换的异常是 SQLException。所以,应该在运行时抛出 ClassCastException。

实际上,这个 java 错误允许为任何标准异常处理代码创建一个不安全的 hack,如下所示:

try 
 doCall();
 catch (RuntimeException e) 
 handle();

异常会在不被处理的情况下上升。

【讨论】:

对我来说,“可能的结果”留有解释的余地​​。我认为如果&lt;E&gt; 绑定到未经检查的异常,则不存在这样的“可能的结果”。请注意,不安全的演员(E) e 隐藏在doThrow0() 中,因此对doThrow() 不可见。我并不是说这一切都很好,但我不明白为什么 JLS 会被这些甚至没有提到泛型的简单规则违反......【参考方案5】:

我认为 JLS 允许该代码是无可争议的。问题不在于该代码是否应该合法。编译器根本没有足够的信息来发出错误。

这里的问题是调用泛型抛出方法时发出的代码。编译器当前在调用泛型-返回方法后插入类型转换,其中返回的值将立即分配给引用。

如果编译器需要,它可以通过在 try-catch-rethrow 片段中静默地包围所有泛型抛出方法调用来防止问题。由于将捕获异常并将其分配给局部变量,因此强制类型转换。因此,如果不是RuntimeException 的实例,您将获得ClassCastException

但规范并没有规定任何关于泛型抛出方法的特殊要求。我相信这是一个规范错误。为什么不提交一个关于这个的错误,以便它可以被修复或至少记录在案。

顺便说一句,ClassCastException 会抑制原始异常,这可能会导致难以发现的错误。但是从 JDK 7 开始,这个问题就有了simple solution。

【讨论】:

我同意 javac 应该添加运行时类型检查以及早发现此问题。但这可能并不总是可能的 - 如果目标类型是通用的,则无法完全完成检查,问题可以隐藏到以后。 @irreputable 对,如果目标类型是泛型,则不会立即强制转换。但是调用者本身是泛型抛出,只能称为泛型抛出。编译器将检查后一个调用站点,并在必要时将其包装在 try-catch-rethrow 中,依此类推。

以上是关于JLS 的哪些部分证明能够像未经检查一样抛出已检查异常?的主要内容,如果未能解决你的问题,请参考以下文章

使用 Mockito 从模拟中抛出已检查的异常

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

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

如何处理未经检查的异常?

我怎么知道我是不是没有处理我的 .NET 代码可能抛出的一些未经检查的异常?

kotlin基础 空值检查