近日在阅读 Hazelcat Jet 代码的时候无意间看到了下边这样一段代码
@SuppressWarnings("unchecked")
public static <T extends Throwable> RuntimeException sneakyThrow(Throwable t) throws T {
throw (T) t;
}
在实际编写代码的时候,可以像下面代码段这样绕过 Java 编译器检查 checked 异常在签名中的要求
@Override
public void close() {
if (state == CLOSE) {
try {
closeFuture.get();
} catch (Exception e) {
throw sneakyThrow(e);
}
} else if (state != END) {
closeProcessor();
}
}
可以看到,我们实际抛出了 Exception
,但是 close
方法并未标注 throws Exception
,可是编译仍然通过了,并且运行时如果进入到异常路径,抛出的也是原始的异常。
这是为什么呢?我们先做几个实验。
public static void main(String[] args) {
sneakyThrow(new Exception());
}
// 1
private static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
throw (T) t;
}
// 2
private static <T extends Throwable> void sneakyThrow(T t) throws T {
throw (T) t;
}
// 3
private static <T extends RuntimeException> void sneakyThrow(Throwable t) throws T {
throw (T) t;
}
// 4
private static <T extends IOException> void sneakyThrow(Throwable t) throws T {
throw (T) t;
}
在上面的三个例子中,1 在运行时抛出 Exception
异常,2 提示没有处理 Exception
异常,3 在运行时抛出无法 cast 成 RuntimeException
异常,4 提示没有处理 IOException
。可以看到,这里的主要矛盾点有两个,第一个是方法签名中的 throws T
如何推断的问题,第二个是 (T) t
强制类型转换如何发生的问题。我们分开来讲。
对于编译器来说,是否要求 sneakyThrow
的调用点检查异常,取决于 throws T
中的 T 被推断为什么类型。可以看到,在 1 和 3 中,T 只出现在 throws T
中,有唯一的约束 T extends Throwable
,对于实际的调用点而言,Exception
的对象被 cast 成 Throwable
。由子类转换成父类是个必定成功的动作,而此动作也不会影响 T 的类型推断。因此对于 T 的类型推断,实际上只有 T extends Throwable
这一个。在这种情况下对 T 的推断在 Java 8 之后有明确的语言规范来限制。
18.1.3 Bounds 一节中提到
A bound of the form
throws α
is purely informational: it directs resolution to optimize the instantiation of α so that, if possible, it is not a checked exception type.
而在 18.4 Resolution 一节中提到
… Otherwise, if the bound set contains
throws αi
, and the proper upper bounds of αi are, at most,Exception
,Throwable
, andObject
, then Ti =RuntimeException
.
简单说来,抛出异常会被尽可能的推断为一个非 checked 的异常。具体到 1 和 3 中,T 在调用点没有实际的类型约束,通过 extends
施加的类型约束属于规范中的范围,在这种情况下,T 会被推断成 RuntimeException
。2 中 T 有实际调用的参数类型限制为 Exception
,或者额外的类型约束不属于规范中的范围,这两种情况下 T 分别被推断为 Exception
和 IOException
,因此编译器分别报出异常。
这是 Java 8 中一个有意为之的特性,具体的讨论可以参考这里和这里。
第二个问题,即强制类型转换,这个也是编译器编译逻辑相关的问题。这里的差别在于 1 和 3,1 里面,Throwable
类型的变量 t
被作为 Throwable
类型的子类型 T
抛出,由于类型擦除的原因,在编译的时候编译器只知道 T
是 Throwable
的子类型,因此无法放置合适的类型转换,转换到自身类型或父类型是不需要任何 cast 的。而在 3 中,编译器明确的知道抛出的是一个 RuntimeException
的子类型,而输入的是父类型 Throwable
,因此可以推断出在 throw
之前可以放置 cast 到 RuntimeException
的类型转换。
最后,代码层面上还有一个小问题。Hazelcat Jet 的代码返回值是 RuntimeException
而实验代码是 void
,这其中的差别在于你更喜欢 throw sneakyThrow(e)
还是 sneakyThrow(e)
。因为方法里面直接就抛异常了,理论上返回值写啥都行,只要能忽悠过编译器就可以。
转载:https://blog.csdn.net/weixin_44723515/article/details/105294716