Java基础知识总结(四)——异常与异常处理表

要想弄清楚异常的一些知识点,比如try/finally的return,异常的丢失等等,光记住“形式”是不够的,不从字节码异常表的层面分析是很难真正的将一些问题弄明白。

先吃两个栗子

(1)判断程序的返回值:

private int hasException() {
        int x;
        try {
            x = 1;
            return x;
        } catch (Exception e) {
            x = 2;
            return x;
        } finally {
            x = 3;
        }
    }

结果:
(1)未发生异常,返回1;
(2)发生Exception异常返回2;
(3)发生其他异常,没有返回值;

以正常返回为例分析:
Java中返回值实际上是由一个returnValue变量保存的,编译器会将finally中的逻辑查到return之前,通过查看字节码可以知道,变量x等于1时,赋值给了returnValue变量,之后才赋值为3的,因此最后返回1。

0: iconst_1 //常量1压入操作数栈
1: istore_1 //x=1,保存到局部变量表slot 1
2: iload_1 //局部变量表slot 1也就是x压入操作数栈
3: istore_2 //栈顶值“1”保存到slot 2,returnValue = 1
4: iconst_3 //常量3压入操作数栈
5: istore_1 //栈顶值“3”保存到局部变量表slot 1, x=3
6: iload_2 //returnValue压入操作数栈
7: ireturn //返回栈顶元素,returnValue的值

(2) finally中的return:

private int finallyReturn() {
    int x;
    try {
        x = 1;
        return x;
    } catch(Exception e) {
        x = 2;
        return x;
    } finally {
        x = 3;
        return 3;
    }
}

结果:
正常返回或者Exception异常或者其他异常都返回3;

分析:
正如前叙,编译器会将finally中的逻辑插入各个分支之中。因此try和catch中的return都不会执行到(被编译器忽略了)。查看字节码:

6: iconst_3 //常量3压入操作数栈
7: ireturn //返回栈顶元素3

PS:
能看懂字节码,很多问题轻松弄明白。
上述字节码使用javap -verbose -private xxxx输出查看。
后面finally子句小节中有更系统的解释。

1. 异常体系

基类:Throwable;
子类划分:Error(内部错误/资源耗尽错误),Exception(又又子类RuntimeException);

是否为受检查异常:
checked异常:Exception,因为为一些不可预测的情况导致的异常,checked的目的是为了让我们能够给出相应的恢复手段;
unchecked异常:Error和RunntimeException,后者一般是“程序员自己的问题导致的”;

声明checked异常:

throws关键字;
class文件中可以通过方法表的Exceptions属性查看(这是javap输出的,实际上Exceptions中保存的是异常类全限定名在常量池中的索引):

Exceptions:
throws java.io.IOException, java.sql.SQLException

声明异常与继承:

子类重写父类方法,如果父类方法声明了异常,子类方法可以:
(1)不声明异常;
(2)声明父类方法中异常或其子类异常;

private static class SuperClass {
    protected void f() throws IOException {}
}

private static class SubClass1 extends SuperClass {
    @Override
    protected void f() throws ZipException {}
}

private static class SubClass2 extends SuperClass {
    @Override
    protected void f() {}
}

2. 捕获异常

在catch语句中重新包装异常,通过设置initCause的方式保证不丢失原始异常:

private void catchAndThrowNew() throws Throwable {
    try {
        throw new SQLException();
    } catch (SQLException e) {
        Throwable se = new ServletException();
        se.initCause(e);
        throw se;
    }
}

finally子句:

(1)与return的关系:

正如开头的例子:
(1)当try子句中有return语句,编译器将产生一个中间变量“returnValue”,保存try中return语句中变量在try语句中的最后赋值;
(2)当finally子句中有return语句,将“覆盖”try中的return;
(3)当finally子句中没有return语句,try中的return因为有returnValue值,不会受finally的影响;

(2)嵌套的finally子句:

(1)如果不同层次的finally子句中包含return,内层的return语句将被忽略,最外层的return才有效;
(2)如果一个内层的finally子句中包含return,外层还有finally子句(没有return),编译器同样会生成一个“returnValue”中间变量,保证内层的return逻辑,可见Java设计的思想在于保证“可见”逻辑的正确性;
(3)执行顺序是先内后外;

private int nestedTryCatch() {
    int x;
    try {
        try {
            x = 2;
        } finally {
            x = 3;
            return x;
        }
    } finally {
        x = 4;
        return x;
    }
}
(3)finally子句与AutoClosable接口:

JSE 7中提供了“try-with-resources”的简化形式:

private void tryWithResources() throws IOException {
    try(InputStream in = new ByteArrayInputStream(new byte[10])) {
        try (Reader reader = new BufferedReader(new InputStreamReader(in))) {

        }
    }
}

注意:带资源的try语句也可以有自己的catch和finally,但是要在资源关闭之后执行。

(4)finally与丢失异常:

情形1:finally中的新异常:

    try {
        throw new IllegalArgumentException();
    } finally {
        throw new IllegalStateException();
    }

情形2:finally中的return:

    try {
        throw new IllegalArgumentException();
    } finally {
        return;
    } 

情形3:finally中没有新异常和return:

    try {
        throw new IllegalArgumentException();
    } finally {
        int x = 3;
    }
字节码分析:

首先,athrow指令是将栈顶异常引用抛出:
(1)情况1的字节码:

Code:
  stack=2, locals=2, args_size=1
     0: new           #21                 // class java/lang/IllegalArgumentException
     3: dup
     4: invokespecial #22                 // Method java/lang/IllegalArgumentException."<init>":()V
     7: athrow
     8: astore_1
     9: new           #23                 // class java/lang/IllegalStateException
    12: dup
    13: invokespecial #24                 // Method java/lang/IllegalStateException."<init>":()V
    16: athrow
  Exception table:
     from    to  target type
         0     9     8   any

分析:
从异常表中可以看到,0到9行中如果有任何的异常出现,转到8行进行处理,第8行之后就是finally之后的逻辑重新抛出了新的异常;

(2)情形3字节码分析:

Code:
  stack=2, locals=3, args_size=1
     0: new           #21                 // class java/lang/IllegalArgumentException
     3: dup
     4: invokespecial #22                 // Method java/lang/IllegalArgumentException."<init>":()V
     7: athrow
     8: astore_1
     9: iconst_3
    10: istore_2
    11: aload_1
    12: athrow
  Exception table:
     from    to  target type
         0     9     8   any

分析:异常表的逻辑和情形1是相同的,但是finally的逻辑对应的字节码将局部变量表中的异常引用取出执行了athrow;

因此,可以说finally的return和异常覆盖内层或者try中的return和异常

3. 堆栈跟踪

(1) Throwable的方法:printStackTrace和getStackTrace;
(2)Thread.getAllStackTraces();

4. 异常使用技巧

(1)不能用异常处理代替逻辑判断,因为异常处理的性能开销较大;
(2)不要过分细化异常及处理;
(3)不要压制异常,异常要么声明,要么正确在该方法内处理;
(4)设计具有意义的自定义异常体系;

PS:其他一些好的做法向《Efftive Java》中说的很详细。

5. 关于Java中的断言和日志

就我的经验来看,还是使用开源的框架中的断言(比如Spring Framework中的断言)和日志(比如Log4j)比较合适。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值