牛刀小试!Java中消除对受查异常的检查

牛刀小试!Java中消除对受查异常的检查

最近在阅读 Java 卷I,并尝试着做一些笔记。当然只是搬运工了,大部分都是在抄原文,偶尔有问题也写点见解什么的。对于原文内容,还是很不错的,让我理解了很多东西,有些很难理解的地方,或者生动的例子,我就直接大量copy了,毕竟我没办法精炼的说出来,让读者或者以后的我去理解。

好了,话不多说,切入主题吧!

今天看到了 卷I 的第8章,是关于泛型的介绍的,其中有一部分和异常有关的内容比较晦涩难懂,特别是 8.6.9小节——可以消除对受查异常的检查,真就拔了一爪子的头发了!

先来看看实际场景:

消除异常的场景

private void testException() {
    Scanner in = null;
    try {
        in = new Scanner(new File("ququx"), "UTF-8");
        while (in.hasNext())
            System.out.println(in.next());
    } catch (Throwable e) {
        e.printStackTrace();
    }
}

比如我有一个 testException() 方法,这个方法可能是从类或接口继承来的,在这个方法中没有声明抛出异常的语句,所有不能在这个方法里面抛出任何受查异常,需要捕获所有受查异常。

但是我并不希望在这个 testException() 方法捕获异常,而是想让上层调用 testException() 方法的地方执行错误处理,那应该怎么办呢?

吐槽一下CSDN,刚写的很详细的博客点击保存草稿就丢了,真是心里万马奔腾,一切重写!

消除异常的方法

@SuppressWamings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T {
	throw (T) e;
}
//编译器就会认为 t 是一个非受查异常。
Block.<RuntimeException>throwAs(t);

这个是 卷I 中提到的使用泛型消除异常的方法,这里提供了一个静态方法,使用了一个继承Throwable泛型参数,并将传入的 Throwable 异常假装强制转换了一下(为什么是假装强制转换,请看下文)并返回。

实际使用也很简单,卷I 上面的例子倒是有点复杂,简单的说就是先捕获异常定,再将其传入确定泛型为RuntimeException的方法中就OK了。

意义什么的大概就是我们的目的吧!

消除异常的使用意义

在声明为不抛出任何受查异常的方法中,不去捕获方法中的受查异常、包装到非受查异常,只是抛出异常,并“哄骗” 编译器,让它认为这不是一个受查异常。

为什么要使用泛型呢?还有其他什么方法吗?确实有

消除异常的普通方法

正常情况下是用一个新的非受查异常包装抛出的异常,将抛出的异常作为非受查异常的“原因”。

RuntimeException t = ...;
try{
	do work
}catch (Throwable realCause) {
	t.initCause(realCause);
	throw t;
}

这和泛型方法挺像啊?有什么区别吗?

我的错误想法

这里是利用了泛型取值的时候不检查类型转换的特点 ,将Throwable 强制转换成了 RuntimeException 并抛出,哄骗编译器,实际抛出的异常就是原来的受查异常!

语气听起来很果断,但是仔细一想问题真多,下面先说说问题吧。

问题一:利用了泛型取值的时候不检查类型转换的特点

真有这样的特点吗?好像没啊,为什么我会这样想。。。

也许是这个:对于一个泛型参数在取值的时候编译器会自动强制转换,甚至可以将父类强制转换成子类。

但是实际是什么样:取出来的泛型参数都是主动放进去的啊,例如:

B是A的子类,我有一个Pair<T extend A>
类型擦除后Pair里面存的是A,但是对于Pair<B>我使用set(B bb)方法时,放进去的实际是B,虽然存在Pair里面是一个A类型,这时候我去取值,编译器会将A转成B是并返回,不会发生类型转换错误。

问题二:将Throwable 强制转换成了 RuntimeException 并抛出,哄骗编译器

真的是这样的吗?强制转换发生了吗?发生在哪里呢?为什么能哄骗编译器呢?

我们先来看下面一个例子。

static class A {}
static class B extends A {}
static class C extends A {}
//试试是否真的不检查类型转换
static <T extends A> T transfer(A e){
    return (T) e;
}
//实际操作
B bb = new B();
C cc = Main.<C>transfer(bb);//ClassCastException: Main$B cannot be cast to Main$C
//此处报错,但错误轨迹未到tranfer函数中去,说明了确实是在取值的时候强制类型转换,但是编译器实际没有忽略类型转换错误的检查。
//那么问题来了,如果不去取值呢?
Main.<C>transfer(bb);//编译能通过,无问题
//结论也就是说,不去取值不会触发编译器强制转换,这点很重要!

这个例子说明两个问题:

  • 那就是编译器是在取值的时候才自动强制类型转换的,当不去取值的时候,编译器不会自动强制转换。
  • 如果发生了自动强制类型转换,将父类赋值给子类有问题,还是会出现类型转换错误的,当然不去取值不会自动转换,自然没问题。

这个例子告诉了我们强制转换到底发生了没、在哪转换的,那为什么能哄骗编译器呢?请看下面例子:

private static void testException() {
    Scanner in = null;
    try {
        in = new Scanner(new File("ququx"), "UTF-8");
        while (in.hasNext())
            System.out.println(in.next());
    } catch (Throwable e) {
        Main.<RuntimeException>throwAs(e);
    }
}
//实际输出FileNotFoundException
try {
    testException();
}catch (RuntimeException e) {
    //实际不生效,catch的还是实际异常
    System.out.println("RuntimeException!") 
//报错不能捕捉 FileNotFoundException,因为没有这个受查异常
}catch (Exception e) {
    System.out.println("FileNotFoundException!");
}

通过这个例子,结合上面例子,这里几个问题,回答之后一切就明了:

最后的正确想法

为什么没有发生 FileNotFoundException 到 RuntimeException 的强制类型转换错误呢?

  • 结合上面例子,因为没拿着 RuntimeException 的变量去要抛出的异常啊!所以根本没有发生强制类型转换。

为什么没强制类型转换还能抛出受查异常呢?

  • 因为 throwAs(e) 方法声明了抛出泛型异常,而此时抛出的泛型的异常是RuntimeException,不是受查异常。编译器认为catch中已经处理了异常,实际并没有处理,只是披个马甲就throw出去了。

为什么可以抛出泛型异常呢?不是不可以抛出泛型异常吗?

  • 对于调用 throwAs(e) 方法,每次异常都是确定,而且并不是抛出泛型异常,只是假装强制转换了一下 (甚至都没生效)。

使用泛型消除对受查异常的检查的总结

  • 对可能发生受查异常的代码块检查,在catch块中调用一个带泛型的静态方法,将受查异常传入该方法。
  • 该静态方法表示会抛出一个泛型的异常,实际抛出的不是泛型异常,是假装强制类型转换的受查异常。
  • 编译器认为catch块中已经处理了受查异常,该静态方法会抛出非受查异常,编译器不会理会,让它抛出。
  • 发生了异常,并抛出了受查异常,由于穿的是非受查异常的马甲,编译器被哄骗了,放过了此异常。
  • 抛出的受查异常并没有用非受查异常的变量去接收,去取值,再次catch会找到它实际的类型,即非受查类型。
  • 到这就消除对受查异常的检查,如果这个代码所在函数没有声明抛出异常,经过这样操作就能够抛出异常了。
  • 在上层调用这代码块的地方,即使方法没有声明抛出异常,也能得到方法可能抛出的异常并进行处理。
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值