angular5 如何抛异常_Sneaky Throw:一种无需声明 Checked 异常的方法

近日在阅读 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)。因为方法里面直接就抛异常了,理论上返回值写啥都行,只要能忽悠过编译器就可以。

再有一点,为什么推断成抛出 unchecked 异常的方法在运行时能够抛出 checked 异常呢?这是因为 checked/unchecked 是 Java 编译器层面的概念,用于在编译时检查出类型错误。在 JVM 或者说字节码的定义里,抛异常就是抛异常,没有什么 checked/unchecked 的区别。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值