夜天之书 #9 Sneaky Throw

确实只有编号的标题表意比较弱,虽然我不起标题以后产量大幅上升,但是本来也会写摘要,摘要拿一点放到标题里也不是不行。

今天时间有限,复刻一篇旧闻,讲的是跟错误处理相关的,算是为之前埋下的错误处理伏笔开个头。

几年前在读 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();
  }
}

我们实际抛出了异常,并且 close 方法并未标注 throws Exception 信息,可是编译仍然通过了。

更重要的是,运行时如果进入到异常路径,抛出的也是原始的异常,而不像常见的用 new RuntimeException(e) 包装的绕过手段,会变成 RuntimeException 在最外层。

这是为什么呢?我们先做几个实验。

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 是唯一的约束。在 sneakyThrow 的调用点上,Exception 参数被 cast 成 Throwable 类型。由子类转换成父类是个必定成功的动作,而此动作也不会影响 T 的类型推断。因此对于 T 的类型推断,实际上只有 T extends Throwable 一个约束。在这种情况下对 T 的推断在 Java 8 之后有明确的语言规范[1]来限制。

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, ExceptionThrowable, and Object, then Ti = RuntimeException.

简单地说,抛出的异常会被尽可能的推断为一个非 checked 的异常。

1 和 3 中,T 在调用点没有实际的类型约束,通过 extends 施加的类型约束属于规范中的范围。这种情况下,T 会被推断成 RuntimeException 类型。

2 中 T 被实参限制为 Exception 类型,4 中 T 的类型约束 T extends IOException 不属于规范中的范围,这两种情况下 T 分别被推断为 Exception 和 IOException 类型,因此编译器分别报出异常。

这是 Java 8 中一个有意为之的特性,具体的讨论可以参考邮件列表的讨论[2]和 StackOverflow 的讨论[3]

第二个问题,(T) t 强制类型转换如何发生,这个也是编译器编译逻辑相关的问题。这个问题只跟 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) 的形式。因为方法里面直接就抛异常了,理论上返回值类型写啥都行,只要能忽悠过编译器就可以。

References

[1] 语言规范: https://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html
[2] 邮件列表的讨论: http://mail.openjdk.java.net/pipermail/lambda-dev/2013-February/007981.html
[3] StackOverflow 的讨论: https://stackoverflow.com/questions/31316581/a-peculiar-feature-of-exception-type-inference-in-java-8

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值