29

最近发现并在博客中提到了这样一个事实,即可以通过 javac 编译器偷偷检查一个已检查的异常并将其抛出到不应抛出的地方。这在 Java 6 和 7 中编译并运行,抛出一个SQLExceptionthrowsorcatch子句:

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()'s<E>必然RuntimeExceptiondoThrow(). 因此,不需要遵循JLS §11.2throws条款。doThrow()
  • RuntimeException与 赋值兼容,因此编译器不会生成Exception强制转换(这将导致 a )。ClassCastException
4

5 回答 5

9

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

泛型作者的回答很可能是“如果您使用未经检查的演员表,那是您的问题”。

我在你的发现中看到了一些非常积极的东西——突破了检查异常的堡垒。不幸的是,这不能将现有的受检查异常中毒的 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 次后,收益就会累积起来。

免责声明

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

于 2012-09-25T10:34:47.673 回答
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();
 }
}

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

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

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

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

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

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

static <E extends Exception> void doThrow0(E e) throws E {
于 2012-09-25T11:44:01.340 回答
2

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

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

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

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

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();
    }
}
于 2012-09-25T10:11:40.150 回答
1

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

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

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

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

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

于 2012-09-25T14:40:15.270 回答
1

JLS 11.2:

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

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

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

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

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

于 2012-09-25T10:50:00.587 回答