RuntimeException sneakyThrow(Throwable t) throws T { throw (T) t; } 在实际编写代码的时候,可以像下面代码段这样绕过" />

小言_互联网的博客

Sneaky Throw:一种无需声明 Checked 异常的方法

195人阅读  评论(0)

近日在阅读 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, and Object, then Ti = RuntimeException.

简单说来,抛出异常会被尽可能的推断为一个非 checked 的异常。具体到 1 和 3 中,T 在调用点没有实际的类型约束,通过 extends 施加的类型约束属于规范中的范围,在这种情况下,T 会被推断成 RuntimeException。2 中 T 有实际调用的参数类型限制为 Exception,或者额外的类型约束不属于规范中的范围,这两种情况下 T 分别被推断为 ExceptionIOException,因此编译器分别报出异常。

这是 Java 8 中一个有意为之的特性,具体的讨论可以参考这里这里

第二个问题,即强制类型转换,这个也是编译器编译逻辑相关的问题。这里的差别在于 1 和 3,1 里面,Throwable 类型的变量 t 被作为 Throwable 类型的子类型 T 抛出,由于类型擦除的原因,在编译的时候编译器只知道 TThrowable 的子类型,因此无法放置合适的类型转换,转换到自身类型或父类型是不需要任何 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
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场