【J2SE】多了解一些异常,多写出一些正常

目录:

  1. Java异常的基础知识;
  2. 异常处理:catch和throws,对方法Code属性中的异常表Exception table和方法的Exceptions属性;
  3. 如果异常没有被处理,那么走兜底的未捕获异常流程,对应Java.lang.Thread.UncaughtExceptionHandler接口;
  4. 异常和return:得出的结果是一定不要在finally中写return代码,因为会导致异常信息被吞掉!

 

一、基础

如何处理程序出错?程序不可能永远不出错,让程序员穷举可能出错的所有情况是不现实的,一是真的做不到,二是即便能做到,大量处理程序出错的非业务代码和业务代码混杂在一起,程序可阅读性大受影响,后期基本上就是灾难。

编程语言的错误处理基本上有两种范式:

  • 一种是使用返回值:C、shell等可为代表,既然无法穷举所有可能的出错情况,那就反其道而行之,只要不是正确情况,都认为是错误情况,通常返回值为0为正常,非零为异常;
  • 另一种是出错则跳转:Java为代表,将可能出错的代码包起来,然后编写错误处理代码,出错则跳转;

Java异常的基础就不赘述了,列一列就行了:

  1. 关键字:try、catch、finally、throw、throws
  2. 异常体系:Throwable、Error、Exception、RuntimeException和非运行时的CheckedException;另外值得一提的是Error也是能够被捕获的,JDK中在java.nio.DirectByteBuffer的构造函数中在分配native memory的时候就捕获了OutOfMemoryError,参看以下源码截图
  3. Java7的多异常捕获
  4. Java7的自动关闭资源:try中包含的类需要实现java.lang.AutoCloseable接口,接口的close函数抛出Exception;如果是抛出IO异常的,可以实现其子类Closeable接口,其close方法抛出IOException;
  5. checked异常和runtime异常:checked异常必须被处理,要么被catch处理,不知道怎么处理那就向上一级throws;
  6. 自定义异常
  7. 抛出异常和异常链
  8. 获取方法的栈帧信息:Throwable类中的StackTraceElement[]就是当前线程栈中的方法调用链的栈帧信息,在出现错误时建立错误对象,会在Throwable的构造函数中调用fillInStackTrace()函数,将当前线程的栈帧信息填充到Throwable.SttackTraceElement[]中
    1. 所以,除了异常对象能够拿到栈帧信息,直接通过线程对象也能拿到:Thread.getStackTrace()也能拿到StackTraceElement[]

【J2SE】多了解一些异常,多写出一些正常

 

二、处理异常:异常表和Exceptions属性

流程

程序执行抛出一个异常(无论是通过throw关键字显式抛出,还是系统执行后检测到错误然后抛出),如果该异常被catch,那么JVM会查询方法Code属性中的异常表寻找异常处理器处理;如果该异常被throws,方法的Exceptions属性中会列出throws声明的异常类型,异常被提交到上一级处理;如果异常既没有被catch也没有被throws,那么该异常会被提交到线程的UncaughtExceptionHandler进行未捕获异常处理(未捕获异常在第三节详谈)

1.catch处理

知识点主要来源《Java虚拟机规范》:

  1. 查表处理在2.10章节有介绍;
  2. 显式抛出异常的指令athrow在2.11.9章节有简介和6.5章节有详解
  3. 异常表结构在4.7.3章节有详解
  4. 异常处理的字节码讲解在3.12章节有详解

看一个例子:

    public void dealException(int control) {
	    try {
	        if (control < 0)
	    	    throw new Exception();
	        if (control == 0)
	        	throw new IOException();
    	} catch (IOException e) {
	        e.printStackTrace();
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
    }

对应的字节码:

【J2SE】多了解一些异常,多写出一些正常

字节码逻辑是这样:

  • iload_1:从局部变量表slot 1加载control的值
  • ifge 12:如果control的值大于等于0,那么跳转到字节码12行执行,跑第二个if
  • new、dup、invokespecial:new指令是创建一个对象,包含分配内存和默认初始化,这里是创建Exception对象,然后dup插入栈顶,通过invokespecial指令调用Exception的构造函数<init>进行构造函数的初始化
  • athrow:将上面创建好的异常对象显式的抛出

假设这个异常抛出后被系统检测到,此时系统会到异常表中进行查询,寻找异常处理器来处理异常,此时的逻辑是这样的:

  • 注意:异常表中的from和to表示异常代码的偏移范围,但是包含from不包含to
  • 此时会逐行的查询异常表的from和to,如果出错代码在[from,to)的范围,那么查看抛出的异常类型和异常表中的type是否一致
  • 如果一致,则跳转到异常表target处代码执行异常处理程序
  • 如果不一致,则继续往下查找
  • 如果没找到,走未捕获异常流程(见第三部分)

对于本例,抛出异常的字节码是第11行,查询异常表属于from~to即0~24行字节码范围,type是Exception,就跳转到target 32行字节码处进行异常处理。本例中就是简单的打印栈帧信息,对应的字节码逻辑是这样的:

  • astore_2:a表示是引用类型,store表示存储到方法Code属性的局部变量表(见上文截图的LocalVariableTable),2表示在局部变量表的第二个slot,在进行异常处理之前先把异常对象存到局部变量表然后再处理异常,后续你是继续抛出还是调用异常对象打印栈帧信息都可以
  • aload_2:a表示引用类型,load表示从局部变量表加载,2表示第二个slot,所以这一条指令的意思是从局部变量表第二个slot加载一个引用类型的对象到栈顶,也就是把刚才存入Exception加载到栈顶
    • 在截图所示的局部变量表中,好像slot 2即存放了IOException又存放了Exception对象,其实不是,因为两个异常对象的抛出都值会在try块代码中,抛出一个代码的执行流程就跑到catch块,不会再回去了,所以运行时slot 2只会存放要给对象,要么是IOException,要么是Exception
  • invokevirtual #25:上面aload_2把异常对象加载到栈顶,然后通过invokevirtual调用异常对象的printStackTrace()函数打印栈帧信息,#25表示调用的方法信息在常量池中
    • #25:#25 = Methodref          #16.#23        // java/lang/Exception.printStackTrace:()V

【J2SE】多了解一些异常,多写出一些正常

最后一个问题是异常表中的异常顺序和代码中的异常顺序不一致,虽然本例中两个if抛出异常的顺序和异常表不一致,但是异常表查表顺序其实一定和catch块的顺序是一致的,“小异常”靠前,“大异常”靠后,如果catch块顺序不是这样的,那么编译一定报错。

2.throws处理

知识点主要来源《Java虚拟机规范》:

  • 4.7.5 方法的Exceptions属性

看一个例子:

    public void dealThrows() throws IOException {
	    FileInputStream fis = new FileInputStream(new File(""));
	    fis.close();
    }

对应的字节码:

【J2SE】多了解一些异常,多写出一些正常

这时候方法的Code属性中就没有再包含异常表了,而是方法有一个Exceptions属性,列出了throws声明的异常,这时候的处理流程:

  • 程序抛出一个异常,系统发现和方法Exceptions中的类型一致,那么就向上抛出,让上一级函数来做异常处理
  • 如果不匹配,或者上一级没有处理异常,那么就走未捕获异常处理流程(见第三部分)

总结一下Java的异常处理:

  • 对于runtime异常,Java认为是你编写的代码有问题,得改到正确为止,所以编译器不会检查是否为RuntimeException提供了异常处理器;
  • 对于checked异常,Java认为虽然程序没有错误,但是有些错误无法避免(像IO),所以你调用的时候不但需要提醒你可能会出什么错,而且编译器还要检查你是否提供了对应的异常处理器;
    • 此时checked异常一定要被处理,要么catch,要么throws
      • 对于catch,需要查询异常表from,to,type,匹配后跳转到target处执行异常处理器
      • 对于throws,需要通过方法的Exceptions属性声明,然后将异常对象提交到上一级进行处理
  • 不管是runtime异常,还是checked异常,对于没有处理到的异常,走未捕获异常流程(见第三部分)

 

三、未捕获的异常

先看一个小例子:【并发编程】java.lang.Thread.UncaughtExceptionHandler

看一下Java api documentation Interface Thread.UncaughtExceptionHandler中的介绍:

Interface for handlers invoked when a Thread abruptly terminates due to an uncaught exception.

When a thread is about to terminate due to an uncaught exception the Java Virtual Machine will query the thread for its UncaughtExceptionHandler using Thread.getUncaughtExceptionHandler() and will invoke the handler's uncaughtException method, passing the thread and the exception as arguments. If a thread has not had its UncaughtExceptionHandler explicitly set, then its ThreadGroup object acts as its UncaughtExceptionHandler. If the ThreadGroup object has no special requirements for dealing with the exception, it can forward the invocation to the default uncaught exception handler.

这个接口有什么用?根据API文档描述,如果一个线程因为未捕获异常而突然终止,那么Thread.UncaughtExceptionHandler就是线程调用处理这种情况的处理器。API文档中接着还介绍了,你需要提前设置实现了Thread.UncaughtExceptionHandler接口的未捕获异常处理器,如果你没设置,那么Thread Group将会作为未捕获异常处理器。

    /**
     * Interface for handlers invoked when a {@code Thread} abruptly
     * terminates due to an uncaught exception.
     * <p>When a thread is about to terminate due to an uncaught exception
     * the Java Virtual Machine will query the thread for its
     * {@code UncaughtExceptionHandler} using
     * {@link #getUncaughtExceptionHandler} and will invoke the handler's
     * {@code uncaughtException} method, passing the thread and the
     * exception as arguments.
     * If a thread has not had its {@code UncaughtExceptionHandler}
     * explicitly set, then its {@code ThreadGroup} object acts as its
     * {@code UncaughtExceptionHandler}. If the {@code ThreadGroup} object
     * has no
     * special requirements for dealing with the exception, it can forward
     * the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler
     * default uncaught exception handler}.
     *
     * @see #setDefaultUncaughtExceptionHandler
     * @see #setUncaughtExceptionHandler
     * @see ThreadGroup#uncaughtException
     * @since 1.5
     */
    @FunctionalInterface
    public interface UncaughtExceptionHandler {
        /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         * @param t the thread
         * @param e the exception
         */
        void uncaughtException(Thread t, Throwable e);
    }

其实逻辑很简单,第二节介绍了jvm是怎么处理异常的,对于没有处理的异常还是有未捕获异常处理器来兜底。未捕获异常的所有相关信息,都在Java.lang.Thread中:

【J2SE】多了解一些异常,多写出一些正常

【J2SE】多了解一些异常,多写出一些正常

java.lang.Thread中有两个UncaughtExceptionHandler对象,一个是实例属性uncaughtExceptionHandler,一个是静态属性defaultUncaughtExceptionHandler:

【J2SE】多了解一些异常,多写出一些正常

你可以通过setUncaughtExceptionHandler(UncaughtExceptionHandler eh)赋值给实例属性uncaughtExceptionHandler,设置某一个线程对象的未捕获异常处理器:

【J2SE】多了解一些异常,多写出一些正常

如果你觉得一个一个设置太麻烦了,那么你可以通过static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh)赋值给静态属性defaultUncaughtExceptionHandler,让该类的所有对象都拥有相同的未捕获异常处理器,当然前面的实例属性优先级更高,毕竟个性化:

【J2SE】多了解一些异常,多写出一些正常

好了,如果你没有调用这些方法显示设置未捕获异常处理器(未捕获异常处理器需要你自己实现UncaughtExceptionHandler接口中的uncaughtException方法),那么Java提供了一个默认的未捕获异常处理器ThreadGroup。

下面让我们来走一走未捕获异常的流程吧:

1.当系统发现一个没有被捕获处理的异常时,JVM会调用包含异常的线程对象的方法dispatchUncaughtException(),这个方法会将该未捕获异常分发给对应的未捕获异常处理器

【J2SE】多了解一些异常,多写出一些正常

2.在dispatchUncaughtException方法中,通过调用getUncaughtExceptionHandler()方法会先找到该线程对象对应的未捕获异常处理器,如果线程对象设置了实例属性uncaughtExceptionHandler,那么就调用该实例属性,否则调用兜底的ThreadGroup。实例属性uncaughtExceptionHandler对应的未捕获异常处理器是需要我们自己实现java.lang.Thread.UncaughtExceptionHandler接口的uncaughtException方法

【J2SE】多了解一些异常,多写出一些正常

3.如果我们提供了实现java.lang.Thread.UncaughtExceptionHandler接口的未捕获异常处理器,那么就走我们自定义未捕获异常逻辑;否则走默认逻辑,走到ThreadGroup.uncaughtException()方法

【J2SE】多了解一些异常,多写出一些正常

4.在默认的未捕获异常处理ThreadGroup,如果有父线程组,则线上提交处理;否则查看有没有线程类的静态未捕获异常处理器(这个也需要我们自己实现),如果有则提交给线程类的未捕获异常处理器处理;否则调用标准错误打印异常信息

在整个流程中,有三个未捕获异常处理器:

  1. 线程对象的未捕获异常处理器uncaughtExceptionHandler,需要自定义实现处理逻辑;
  2. 系统默认的未捕获异常处理器ThreadGroup
  3. 线程类的未捕获异常处理器defaultUncaughtExceptionHandler,也需要自定义实现处理逻辑;

简单来说,这个流程就是:

  1. JVM首先会将未捕获异常分发给未捕获异常处理器
  2. 如果线程对象设置了自定义的未捕获异常处理器,那么交给自定义的处理器处理
  3. 如果线程对象没有设置自定义的未捕获异常处理器,那么交给默认的处理器ThreadGroup
    1. 在默认的处理器ThreadGroup中,如果有父线程组则往上提交处理
    2. 否则,如果设置了线程类静态的未捕获异常处理器,则将未捕获异常交给线程类静态的的未捕获异常处理器处理
    3. 否则,通过标准错误System.err打印异常信息

 

四、异常和return

1.finally一定会被执行吗

finally一定会被执行的前提是JVM进程还在,如果在finally被执行之前,调用System.exit退出jvm进程,则finally将不会被执行。

2.异常和return的原理

先看一个小例子:【JVM】异常和return

Java异常关键字的结构有三种:try catch、try finally(运行时异常允许这种结构存在,checked异常需要被catch或throws)、try catch finally,try块是不能单独存在的,所以异常和return可以有以下几种情形:

  • a.try catch
    • 1.只有try中包含return
    • 2.try和catch中都包含return
  • b.try finally
    • 1.只有try中包含return
    • 2.try和finally中都包含return
  • c.try catch finally
    • 1.只有try中包含return:正常流程相当于b1情形(只有try中包含return,执行完try块运行finally),异常流程try中的return不会被执行,运行catch和finally
    • 2.try和catch中包含return:正常流程相当于b1情形(catch不会执行,try中包含return,最后执行finally),异常流程try中的return不会被执行,运行catch中的return之后执行finally
    • 3.try和finally中包含return:正常流程相当于b2情形(catch不会执行,try中包含return,finally中也包含return),异常流程try中的return不会被执行,执行catch后finally
    • 4.try catch finally中都包含return:正常流程try catch,相当于b2情形,异常流程try中的return不会被执行,运行catch和finally

a.try catch

1.只有try中包含return

这种场景,正常流程下遇到return函数返回;异常流程不会执行try中的return,遇到athrow指令显式抛异常或运行时抛出异常,则查异常表处理异常(见第二节的异常处理),又因为异常被处理了,程序不会因为异常而终止,所以在try块之外还需要return让异常修复后的程序正常返回,否则编译报错。

2.try和catch中都包含return

这种场景,正常流程下try块中遇到return函数返回,不会执行catch中的代码;异常流程下,不会执行try块中的return,抛出异常时会查询异常表跳转执行catch中的代码,然后返回catch中的return,代码示例如下:

    // 输出结果是2
    public static int getValueCatch() {
	    int i = 1;
	    try {
	        if (i == 1)
		        throw new Exception("xx");
	        return i;
	    } catch (Exception e) {
	        return ++i;
	    }
    }

对应的字节码,其中iinc 0,1指令的含义是将局部变量表slot 0中的值加1,也就是++i中++运算符号对应的指令(iinc是操作局部变量表某一个slot的指令,其他指令都是操作栈帧的指令,如果自增运算符++的是实例属性或静态属性,那么会被编译成getField/getStatic,然后iadd、dup,最后putField/putStatic)。

【J2SE】多了解一些异常,多写出一些正常

b.try finally

1.只有try中包含return

此时try中的return代码不会编译成ireturn指令,而是将return的值存储到局部变量表其他的slot中,执行完finally的代码后(finally操作的是原来的slot),再从局部变量表中取出值返回,代码示例如下:

    // 输出结果为1
    public static int getValueTry() {
	    int i = 1;
	    try {
	        /**
	         * 当执行完return i的时候,i的值被保存到局部变量表其他slot 等执行完finally之后,才会从该slot从返回
	         */
	        return i;
	    } finally {
	        /**
	         * 此时增加的是局部变量表中原来的i的值 待返回的值被保存到局部变量表中其他的位置,因此这里不影响返回值
	         */
	        i++;
	    }
    }

对应的字节码:

【J2SE】多了解一些异常,多写出一些正常

其中try块中的return i并没有被编译成ireturn指令,而是被istore_2指令存储到了局部变量表的slot 2(原来i的值在slot 0,方法最初的int i=0的代码编译成两条指令,iconst_1是常量值1进栈,istore_0将值1存入局部变量表slot 0,局部变量表slot 0的name就是局部变量i);接着执行finally中的代码i++,编译成指令iinc 0,1,意思是将局部变量表slot 0中的值加1,注意此前return的值被保存到了局部变量表slot 2;最后7、8两行的字节码iload_2加载局部变量表slot 2的值,然后ireturn返回,所以示例代码的返回值是1。

后面9~14行代码是异常处理的,很明显如果try块中出现了异常,将会查异常表然后跳转到第九行字节码执行,此时astore_1将异常对象保存到局部变量表slot 1,然后运行finally中的代码对应的指令iinc 0,1,最后aload_1加载异常对象,然后athrow抛出异常,查表没有对应的异常处理器,方法也没有Exceptions属性,此时会走未捕获异常流程。

2.try和finally中都包含return

此场景下try中的return代码不会被编译成对应的return指令,真正返回的是finally中的return。

此时一定要注意:千万不要在finally中编写return代码,因为这种场景下会吞异常!代码示例如下:

    /**
     * 去掉if throw,输出结果为3,try中的return i++会被优化掉,优化成i++
     * 包含if throw,输出结果为2,因为try中的return代码不会被执行
     * if throw抛出的异常会被优化掉
     * 优化成pop指令将异常对象扔出栈不做处理
     */
    @SuppressWarnings("finally")
    public static int getValueFinally() {
	    int i = 1;
	    try {
	        /**
	         * try中的return在编译的时候就被优化掉了
	         */
	        if (i == 1)
		        throw new RuntimeException();
	        return i++;
    	} finally {
	        // 这里如果去掉return,则异常不会被吞掉
	        return ++i;
    	}
    }

对应的字节码如下:

【J2SE】多了解一些异常,多写出一些正常

正常情况下(去掉示例代码的if throw部分),try块中的return i++代码会被优化成i++,编译成iinc和goto指令,goto到finally的代码继续执行,实际上return的是finally中的值,此时方法的返回值是3。

如果包含if throw,那么try块中的return不会被执行,而是运行if throw抛出一个异常,对应的字节码从第二行到第十四行,14行字节码athrow指令会显式抛出一个异常,此时jvm会查询异常表,然后转到第21行字节码,在这里pop指令会将栈顶的异常对象扔出栈,此时方法栈中就不存在异常对象了,异常被吞掉了,代码运行也不会走未捕获异常处理调用标准错误System.err在console中打印错误信息。再将异常对象pop扔出栈以后,接着执行finally中的代码,对应的指令iinc、iload和ireturn,最后返回值是2并且不抛出异常。

可以和b1中的示例对比来看,b1中的finally没有return,所以b1中代码一旦出异常,会先将栈顶的异常对象保存到局部变量表,然后运行finally的代码,最后取出局部变量表中的异常对象athrow抛出异常。

c.try catch finally 

1.只有try中包含return

正常流程和b1一样;异常流程不会运行try块中的return,执行catch中的代码接着执行finally中的代码,此时又因为异常已经被catch处理了,程序不会因为异常而结束执行,所以在try块之外还需要return。

2.try和catch包含return

正常流程不会执行catch中的return,和b1的情形一致;异常流程不会执行try块中的return,而是在抛出异常后查询异常表,然后跳转到异常处理器执行,catch块中的return值被保存到局部变量表其他slot,然后执行finally代码,最后取出catch块中return保存到局部变量表中的return值,函数返回。这个情形和b1也非常相似,不过那个场景是try-return & finally,try中return的值被保存到局部变量表其他slot,而这一例是catch-return & finally,catch中return的值被保存到局部变量表其他slot。

3.try和finally包含return

正常流程和b2的正常流程一致,try中的return被优化成goto finally的指令,然后return的是finally中的值;异常流程try块中的return代码不会被执行,查询异常表跳转到catch中执行异常处理代码,之后运行finally的代码并返回finally中的return。

4.try catch finally都包含return

正常流程和b2的正常流程一致;异常流程不会执行try块中的return,当抛出的异常是catch的异常,则会查询异常表跳转到catch中执行代码,所以不会像b2 try finally异常流程那样吞异常,catch中的return会被优化掉,优化成goto finally,最终返回的是finally中的return。必须要指出的是:如果出现的异常不是代码中catch的异常类型,那么这个异常也会被吞掉。代码示例如下:

    // try、catch和finally中都有return
    @SuppressWarnings("finally")
    public static int getValueTryCatchFinally(int control) {
	    try {
	        if (control == 1)
	        	throw new Exception("xx");
	        return control;
    	} catch (Exception e) {
	        return ++control;
    	} finally {
    	    return ++control;
	    }
    }

对应的字节码如下:

【J2SE】多了解一些异常,多写出一些正常

如图所示,如果抛出的异常是代码中catch的异常,那么查询异常表就能找到对应的异常处理器,否则查询异常表会跳转到pop指令,将栈顶的异常对象扔掉,然后执行finally的代码。

总结一下:

  • 千万不要在finally中return,因为有可能会吞掉异常信息;
  • 如果finally中有return,那么肯定返回finally中的值,try块或return块中的return会被优化掉;
  • 如果finally中没有return,但是有finally,那么try块或catch块中的返回值会先被保存到局部变量表的其他slot,执行完finally的代码再从局部变量表取出返回,finally中的代码不会影响return值;
  • 如果没有finally,正常流畅返回try块的return,异常流程返回catch的return;

 

总结一下本文内容:

  1. Java异常的基础知识;
  2. 异常处理:catch和throws,对方法Code属性中的异常表Exception table和方法的Exceptions属性;
  3. 如果异常没有被处理,那么走兜底的未捕获异常流程,对应Java.lang.Thread.UncaughtExceptionHandler接口;
  4. 异常和return:得出的结果是一定不要在finally中写return代码,因为会导致异常信息被吞掉!

 

附注:本文如有错漏,烦请不吝指正!

上一篇:J2SE-网络编程


下一篇:复习 J2SE基本内容 常量池问题