JLS 的哪些部分证明能够抛出已检查的异常,就好像它们未被选中一样?

我最近发现并在博客上写道,可以通过javac编译器偷偷出现一个经过检查的异常,并将其扔到不能扔掉的地方。这将在 Java 6 和 7 中编译和运行,并抛出一个 without 或 子句:SQLExceptionthrowscatch

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语言是否应该这样做。JLS的哪些部分证明了这种行为的合理性?这是一个错误吗?还是Java语言的一个隐藏得很好的“功能”?

我的感受是:

  • doThrow0()的 绑定到 中。因此,在 中不需要与 JLS §11.2 类似的子句。<E>RuntimeExceptiondoThrow()throwsdoThrow()
  • RuntimeException与 赋值兼容,因此编译器不会生成强制转换(这将导致 )。ExceptionClassCastException

答案 1

所有这些都相当于利用了一个漏洞,即未经检查的转换为泛型类型不是编译器错误。如果代码包含这样的表达式,则代码将显式设置为类型不安全。由于检查已检查的异常严格来说是一个编译时过程,因此运行时中不会中断任何内容。

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

我在你的发现中看到了一些非常积极的东西 - 突破了检查异常的堡垒。不幸的是,这不能将现有的检查异常中毒的API变成更令人愉快使用的东西。

这如何提供帮助

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

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

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

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

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

免責聲明

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


答案 2

此示例记录在 CERT Oracle 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();
 }
}

这有效,因为 是 的子类,您无法转换从 到 的任何类,但如果您像下面一样转换,它将起作用RuntimeExceptionExceptionExceptionRuntimeException

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

您正在执行的操作与此相同。因为这是显式类型强制转换,所以此调用将导致编译器允许它。ClassCastException

由于擦除,但是在您的案例中,不涉及强制转换,因此在您的场景中,它不会抛出ClassCastException

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

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

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