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

时间:2012-09-25 09:59:41

标签: java exception generics jls checked-exceptions

我有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()的{​​{1}}与<E>中的RuntimeException绑定。因此,doThrow()中不需要JLS §11.2throws条款。
  • doThrow()RuntimeException分配兼容,因此编译器不会生成强制转换(导致Exception)。

5 个答案:

答案 0 :(得分:8)

所有这些都等于利用漏洞,未经检查的强制转换为泛型类型不是编译器错误。如果代码包含这样的表达式,那么它的代码是明确的类型不安全的。由于检查已检查的异常严格来说是编译时过程,因此运行时不会中断任何内容。

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

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

声明

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

答案 1 :(得分:2)

嗯,这是提出检查异常的多种方法之一,就像它未经检查一样。 Class.newInstance()是另一个,Thread.stop(Trowable)已被弃用。

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

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

编辑:根据评论中的讨论,基于列表的示例揭示了根本原因:Erasure

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();
    }
}

答案 2 :(得分: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的任何类,但如果您按照以下方式进行转换,它将起作用< / p>

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

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

由于Erasure,但在你的情况下没有涉及演员阵容,因此在你的场景中它不会抛出ClassCastException

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

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

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

答案 3 :(得分:1)

JLS 11.2:

  

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

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

从实际角度来看,这个java bug允许为任何标准异常处理代码创建一个不安全的hack,如下一个:

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

异常将在没有处理的情况下上升。

答案 4 :(得分:1)

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

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

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

但该规范并没有强制要求使用泛型投掷方法。我相信这是一个规范错误。为什么不提交相关的错误,以便可以修复或至少记录。

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