一.异常了解
- 异常处理可以使程序中的异常处理代码和正常业务代码分离,保证程序代码更加优雅,并可以提高程序的健壮性。
- 异常处理机制主要依赖于try,catch,throw和throws五个关键字,异常机制会保证finally块总被执行。
- throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常;
- throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象。
异常分类:
- Checked异常
- Runtime异常
java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的Checked异常;而Runtime异常则无须处理。Checked异常可以提醒程序员需要处理所有可能发生的异常,但Checked异常也给编程带来一些繁琐之处,所以Checked异常也是java领域一个备受争论的话题。
抛出异常:如果执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该对象被提交给java运行环境,这个过程被称为抛出异常
捕获异常:当java运行时环境收到异常对象时,会寻找处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交给该catch块处理,这个过程被称为捕获异常。
如果java运行时环境找不到捕获异常的catch块,则运行时环境终止,java程序也将退出。
二.异常体系
- try有且只有一个只能执行一次(try不能单独使用,后跟catch或finally或两者)
- catch可以有多个,理论只有一个被执行
- try块里声明的变量是代码块内的局部变量,它只在try块内有效,在catch块不能访问该变量
- java把所有的非正常情况分成两种:异常(Exception)和错误(Error),它们都继承Throwable父类
- 优先处理小异常,在处理大异常,否则将会出现编译错误
try块后可以有多个catch块,这是为了针对不同的异常类提供不同的异常处理方法。当系统发生不同的意外情况时,系统会生成不同的异常对象,java运行时就会根据该异常对象所属的异常类来决定使用哪个catch块来处理该异常。
Error错误,一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义该方法时,也无须在其throws字句中声明该方法可能抛出Error及其任何子类。
2.1 java7新增的多异常捕获
使用一个catch块捕获多种类型的异常时需要注意如下两个地方:
- 捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开
- 捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值
2.2访问异常信息
所有的异常对象都包含了如几个常用的方法:
- getMessage():返回该异常的详细描述字符串
- printStackTrace():将该异常的跟踪栈信息输出到标准错误输出
- printStackTrace(PrintStream s) :将该异常的跟踪栈信息输出到指定输出流
- getStackTrace():返回该异常的跟踪信息
2.3 使用finally回收资源
有些时候,程序在try块里打开了一些物理资源(例如数据库连接,网络连接和磁盘文件等),这些物理资源都必须显式回收。
java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占有的内存。
异常处理机制提供了finally块,不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。
当java程序执行try,catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throw语句,方法终止;如果有finally块,系统立即开始执行finally块---只有当finally块执行完成后,系统才会再次跳回来执行try块,catch块里的return或throw语句;如果finally块里也使用了return或throw等导致方法终止的语句,finally块已经终止了方法,系统将不会跳回去执行try块,catch块里的任何代码块。
备注:如果catch块里有return语句,但一定会先执行finally块里的代码
如果在异常处理代码中使用System.exit(1)语句来退出虚拟机,则finally块将失去执行的机会
不要在finally块中使用如return或throw等导致方法终止的语句,否则会导致try块,catch块中的return,throw语句失效
2.4 异常处理的嵌套
finally块里也包含了一个完整的异常处理流程,这种在try块,catch块或finally块中包含完整的异常处理流程的情形被称为异常处理的嵌套。
通常没有必要使用超过两层的嵌套异常处理,层次太深的嵌套异常处理没有太大必要,而且导致程序可读性降低。
2.5 java9增强的自动关闭资源的try语句
java7增强了try语句的功能--它允许在try关键字后紧跟一对圆括号,圆括号可以声明,初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显式关闭的资源(比如数据库连接,网络连接等),try语句在该语句结束时自动关闭这些资源。
需要指出的是,为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseble或Closeable接口,实现这两个接口就必须实现close()方法。
try(
BufferedReader br=new BufferedReader(new FileReader("AutoCloseTest.java"));
){
//******************
}
自动关闭资源的try语句相当于包含了隐式的finally块(这个finally块用于关闭资源),因此这个try语句可以既没有catch块,也没有finally块。
java7几乎所有的资源类(包括文件IO的各种类,JDBC编程的Connection,Statement等接口)进行了改写,改写后资源类都实现了AutoCloseable或Closeable接口。
java9再次增强了这种try语句,java9不要求在try后的圆括号内声明并创建资源,只需要自动关闭的资源有final修饰或者是有效的final(effectively final),java9允许将资源变量放在try后的圆括号内。
//有final修饰的资源
final BufferedReader br=new BufferedReader(new FileReader("AutoCloseTest.java"));
//没有显式使用final修饰,但只要不对该变量重新赋值,该变量就是有效的final
PrintStream ps=new PrintStream(new FileOutStream("a.txt"));
//只要将两个资源放在try后的圆括号内即可
try(br;ps){
//使用两个资源
}
三.Checked异常和Runtime异常体系
3.2 方法重写时声明抛出异常的限制
方法重写时“两小”中的一条规则:
- 子类方法声明抛出的异常体系应该是父类方法声明抛出的异常类型的子类或相同
- 子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
使用Checked异常至少存在如下两大不便之处:
- 对于程序中的Checked异常,Java要求必须显示捕获并处理该异常,或者显示声明抛出该异常。这样就增加了编程复杂度。
- 如果在方法中显式声明抛出Checked异常,将会导致方法签名与异常耦合,如果该方法是重写父类的方法,则该方法抛出的异常还会受到被重写方法所抛出异常的限制。
在大部分时候推荐使用Runtime异常,而不使用Checked异常。尤其当程序需要自行抛出异常时,使用Runtime异常将更加简洁。
四.使用throw抛出异常
如果需要在程序中自定抛出异常,则应该使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。throw语句的语法格式如下:
throw ExceptionInstance;
例如:
if(true){
throw new Exception("参数错误");
}
不管是系统自动抛出的异常,还是程序员手动抛出的异常,Java运行时环境对异常的处理没有任何差别。
五.Java的异常跟踪栈
异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。
面向对象的应用程序运行时,经常会发生一系列方法调用,从而形成“方法调用栈”,异常的传播则相反;只要异常没有被完全捕获(包括异常没有被捕获,或异常被处理后重新抛出了新异常),异常从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法调用者再次传给调用者。。。。。。。直至最后传给到main方法,如果main方法依然没有处理该异常,JVM会中止该程序,并打印异常的跟踪栈信息。
第一行的信息详细显示了异常的类型和异常的详细消息。
一行行地往下看,跟踪栈总是最内部的被调用方法逐渐上传,直到最外部业务操作的起点,通常就是程序的入口main方法或Thread类的run方法(多线程的情形)。
虽然printStackTrace()方法可以很方便地用于跟踪异常的发生情况,可以用它来调试程序,但在最后发布的程序中,应该避免使用它。而应该对捕获的异常进行适当的处理,而不是简单地将异常的跟踪栈信息打印出来。
六.异常处理规则
- 使程序代码混乱最小化
- 捕获并保留诊断信息
- 通知合适的人员
- 采用合适的方式结束异常信息
- 采用合适的方式结束异常活动
6.1 不要过度使用异常
异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑代码分离,因此不要使用异常处理代码来代替正常的业务逻辑判断。
异常只应该用于处理非正常的情况,不要使用异常处理来代替正常的流程控制。
6.2 不要使用过于庞大的try块
- try块里的代码过于庞大,业务过于复杂,就会造成try块中出现异常的可能性大大增加,从而导致分析异常原因的难度也大大增加。
- try块过于庞大,紧跟catch块也会增多才可以针对不同的异常提供不用的处理逻辑。反而增加了编程复杂度。
- 正确的做法是,把大量的try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常。
6.3 避免使用Catch All语句
所谓Catch All语句指的是一种异常捕获模块,它可以处理程序发生的所有可能异常。例如:
try
{
//可能引发Checked异常的代码
}
catch(Throwable t)
{
//进行异常处理
t.printStackTrace();
}
这种处理方式有如下两点不足之处:
- 所有的异常都采用相同的处理方式,这将导致无法对不用的异常分情况处理,如果要分情况处理,则需要在catch块中使用分支语句进行控制,这是得不偿失的做法。
- 这种捕获方式可能将程序中的错误,Runtime异常等可能导致程序终止的情况全部捕获到,从而“压制”了异常。如果出现了一些“关键”异常,那么此异常也会被“静悄悄”地忽略。
6.4 不要忽略捕获到的异常
仅在catch块里打印错误跟踪栈信息稍微好一点,但仅仅比空白多了几行异常信息。通常建议对异常采取适当措施,比如:
- 处理异常。对异常进行合适的修复,然后绕过异常发生的地方继续执行;或者用别的数据进行计算,以代替期望的方法返回值;或者提示用户重新操作。。。。总之,对于Chedked异常,程序应该尽量修复。
- 重新抛出新异常。把当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新抛出给上层调用者。
- 在合适的层处理异常。如果当前层不清楚如何处理异常,就不要在当前层使用catch语句来捕获该异常,直接使用throws声明抛出该异常,让上层调用者来负责处理该异常。