有一天群里的同学提了个关于异常逃逸的问题,竟然回答不了,这才发现自己过去有关异常的内容学的实在粗浅~
理论上说,世界上是没有完美的程序的,一个运行良好的程序是需要各种各样的条件和环境的,这些条件就是变量,只有满足这些变量,程序才能把正确运行变成一种常态。如果程序状态异于我们期望的状态就是存在异常,在《Thinking in Java》异常的开篇就有写到:Java的基本理念是“结构不佳的代码不能运行”。
前置概念
异常情形:阻止当前方法或作用域继续执行的问题。
这里需要明确的是,异常处理机制并不能代替我们处理异常,而是提示我们:当前代码块可能或已经存在异常,可能导致程序运行失败。我们需要对可能或已经出现的异常做出处理。当一个方法抛出异常时,会使用new关键字在堆中创建异常对象,然后当前执行的路径被终止(不能继续执行了),并且从当前环境中弹出对异常对象的引用,此时异常处理机制接管程序并开始寻找一个恰当的地方继续执行程序。这个恰当的地方就是异常处理程序,它的作用是将程序从错误状态恢复,以便程序要么换一种方式执行要么继续执行下去。--(节选《Thinking in Java》异常篇)
在上面可以看到,异常发生的同时,会自动的在堆中创建异常对象,异常对象有两种方式获得,一个是java自带的标准异常,另一种是自定义异常,下面先看一下java的标准异常体系。
标准异常
java语言存在很多定义好的标准异常类,我们大部分使用的都是定义好的异常,下面可以来一起看看这个异常体系。
异常体系图.jpg如上图所示,在java中存在一个类Throwable
,它是类Exception和类Error的父类,所有的异常Exception
和错误Error
都是从这里开始的。
Exception
和Error
的区别:Exception
是程序本身的问题,是可以处理的;Error
一般发生在JVM中,例如常见的OutOfMemoryError
,当发生时,JVM会选择停止当前线程,我们并不能对它做出什么处理。
类Exception下还存在很多些子类例如RuntimeException
,子类下面存在的则是所用的具体异常类。特此说明,在上图仅仅画了两个类Exception
的子类,但类Exception
并不仅仅只有两个子类,不过大体上可以分这样两个部分:运行时异常和非运行时异常(检查性异常)。
处理异常
java中有两种方式处理异常,其中一种是抛出异常,在出现问题的地方使用throw
关键字抛出异常对象。如上文所述,抛出异常时会使用new
关键字在堆中创建异常对象,异常对象中包含了异常信息和异常出现的程序状态信息。上一级环境负责处理当前异常信息或者抛到它的上一级环境中处理。例如:
public class ExceptionDemo {
public static void main(String[] arg0){
int i = 3;
int j = 0;
try{
if(j == 0) throw new ArithmeticException();
System.out.println("---正常运行-,i/j的值为:"+i/j);
}catch(Exception e){
System.out.println("---捕获异常,j = 0");
}finally{
}
System.out.println("---正常完成");
}
}
输出:
---捕获异常,j = 0
---正常完成
如上所示,if(j == 0) throw new ArithmeticException
,这里抛出一个异常时会创建一个ArithmeticException
,异常处理机制会捕获这个try
方法内部的异常,进入catch
来处理,最后完成整个流程。
在上面这个例子中,使用条件语句抛出异常,但是还可以在方法上使用throws
关键字抛出异常,内部使用逗号来分隔方法抛出的多个异常,但这里只限于检查性异常。因为在方法上抛出异常,那么就会涉及到子类继承或接口实现而来的方法,这种情况下,就需要子类抛出的Exception
和父类抛出的异常相兼容。规则如下:子类覆盖或实现的方法不允许在throws语句中出现比父类更多的检查型异常,也就是说只能比父类少,不能比父类多。如果某个方法的实现是多重继承的,那么它的实现需要满足所有继承而来的throws字句,简单考虑一下就知道,多态处理代码逻辑时,使用的是父类或者接口定义的规则,那么在程序中是不会处理子类新增的规则的。
类似的,在上面一个例子,我们还看到了try{}catch(Exception e){}finally{}
,这是java异常处理机制的另外一种方式。下面来详细说一下这个我们很常用的try-catch-finally
.
try-catch-finally
这个整体由三部分组成的,其实通常情况下并不是都需要像这样完整,语法要求try
方法块之后必须跟着一部分,要么是catch
块要么是finally
块,当然也是可以两者都存在的。在try
块中,程序会一直运行直到抛出异常或者顺利执行完成,其中try
块一次只能捕获一个异常;当try
块遇到异常时程序终止继续向下执行并进入catch
块中匹配,匹配时会就近选择与捕获的异常相匹配的区域,类似于switch语句的选择机制,只是这里没有default
区域,当找到一个匹配的异常时,就会进入内部执行语句,所以不要将Exception
放在最上面的catch
中,以免所有的异常都直接进入执行,这时候想想,其实也有可能在catch
部分找不到相匹配的异常,那么这个异常会渗透出当前try
块到达任何能处理它的外层try
块对应的catch
部分;而finally
块则是进入try
块后必须执行的部分,不管是try
块以哪种形式执行结束的,通常情况下是用来关闭系统资源的,例如IO
连接。
可以看到try-catch-finally
中的代码是依次执行的,当try
块中有异常时,catch
块会捕获并执行匹配的代码,如果没有catch
,则会把exception
添加到return
栈顶并执行finally块。请记住,java exception栈只会保存最新的一条信息。把try-catch-finally
使用说完以后,我们可以看一下几个比较有趣的问题:
1.如果catch
中,再次抛出exception
,那么原try
中的exception
是否还能抛出呢?
既然已经再次抛出一个exception
了,那么原有的exception
自然是没有意义,而java exception
栈中只会保存最新的一条信息,所以自然是不会抛出try
中的exception
的。
2.如果catch
中,抛出异常,finally
是否继续执行?那么如果在catch
和finaly
中,都使用了return
,那么最终是哪个return
值时有意义的?
当然是会继续执行的。最后那个有意义,如下示例:
/**
***catch和finally中都使用了return
**/
public class ExceptionDemo {
public static void main(String[] arg0){
System.out.println("---test return -- "+test());
}
public static int test() {
try{
System.out.println("try");
throw new RuntimeException("try");
} catch (Exception e) {
System.out.println("catch");
return 1;
} finally {
System.out.println("finally");
return 2;
}
}
}
输出:
try
catch
finally
---test return -- 2
3.如果finally
抛出异常,那么catch
中抛出的异常,还能被外部捕获到吗?
不会,如果finally
中还抛出了异常,那么catch
中再次抛出的会被擦除(异常逃逸)。因为java exception
中只会保留最新的一条exception
信息。
4.如果catch
中,再次抛出异常,那么在finally
中使用return
,外部还能捕获异常吗?
return
和exception
都被认为是“方法中断”操作,最后发生者将会生效;当catch
中再次抛出异常,原目的是将此异常抛给调用者,结果在finally
中使用return
(我们认为此处使用return是不当的,会有警告提示),那么异常将会被擦除,例如:
/**
***catch和finally块中断语句后者为准
**/
public class ExceptionDemo {
public static void main(String[] arg0){
try{
System.out.println("---test return -- "+test());
}catch(Exception e){
e.printStackTrace();
}
}
public static int test() {
try{
System.out.println("try");
throw new RuntimeException("try");
} catch (Exception e) {
System.out.println("catch");
throw new RuntimeException("catch");
} finally {
System.out.println("finally");
return 2;
}
}
}
输出:
try
catch
finally
---test return -- 2
可以看到,输出并没有打印异常信息,所以catch
和finally
两个确实是最后发生的才是生效的那个。
异常链
在捕获一个异常后抛出另一个异常,并希望把原始信息保存下来,这被称为异常链,在代码中,这种情形很常见,例如:
/**
***异常链实例
**/
public class ExceptionDemo {
public static void main(String[] arg0){
ExceptionDemo demo = new ExceptionDemo();
try{
demo.test();
}catch(NoSuchElementException e){
Throwable throwable = e.getCause();
System.out.println("--异常进入 NoSuchElementException"+throwable.toString());
}
System.out.println("---正常完成");
}
public void test(){
try{
int i = 0;
if(i == 0) throw new ArithmeticException();
}catch(ArithmeticException e){
NoSuchElementException noSuchElement = new NoSuchElementException();
noSuchElement.initCause(e);
throw noSuchElement;
}
}
}
输出:
--异常进入 NoSuchElementExceptionjava.lang.ArithmeticException
---正常完成
如上所示,异常NoSuchElementException
是由异常ArithmeticException
引起的,即由另一个异常引发的异常。在Throwable
的子类中可以通过接受一个cause
对象作为参数,这个参数就是用来保存原始异常的,这样在抛出新的异常中也可以通过这个异常链追踪到原始异常。
自定义异常
在java中确实已经存在足够多的标准异常给我们日常使用,但是总会有业务存在需要自定义异常来表述遇到的特定问题,所以我们看看自定义异常需要做什么。
1.在前面已经说过,所有的异常类都是继承于一个基类Throwable,所以自定义异常只能继承类Throwable或者它的子类。
2.实现其中的构造方法。
例如:
/**
***自定义异常
**/
public class MyException extends Exception{
public MyException(){
}
public MyException(String message){
super(message);
}
}
如上所示,一个基本的异常类已经出现了,接下来我们只需要像已经存在的标准类一样使用它即可。
说到这里,异常部分大体已经说完了,可能存在遗漏或错误的地方,请帮忙指出~
本文参考
《Thinking in Java》第12章 通过异常处理错误
《java程序设计语言》第12章 异常与断言
博文:Java中try-catch异常逃逸
博文:java提高篇(十六)-----异常(一)
博文:java提高篇(十七)—–异常(二)
网友评论