异常处理
异常处理机制可以使得程序中的异常处理代码和正常业务代码分离,保证程序代码更加优雅,并可以提高程序的健壮性
异常的概述
对于错误处理机制,主要有两个缺点:
- 无法穷举所有的异常情况。因为人类知识的限制,异常情况总比可以考虑到的情况多,总有“漏网之鱼”的异常情况,所以程序总不够健壮
- 错误处理代码和业务实现代码混杂。这种错误处理和业务实现混杂的代码严重影响程序的可读性,会增加程序维护的难度
异常处理机制
java的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序发生异常,系统会自动生成一个Exception对象来通知程序,从而实现将“业务功能代码”和“错误处理代码”分离,提供更好的可读性。
使用try…catch捕获异常
如果程序顺利完成,那么“一切正常”,把系统的业务实现代码放入try块中定义,所有的异常处理逻辑放在catch块中处理。
//捕获异常语法
try{
//业务功能代码
}
catch(Exception e){
//异常处理逻辑代码
}
如果执行try块里的业务逻辑代码出现异常,系统自动生成一个异常对象,交给java运行时环境,这个国产称为抛出异常。
当java运行时环境接受到异常对象,会寻找能处理该异常对象的catch块,如果找到则交给catch块处理,这个过程称为捕获异常。当找不到捕获异常catch块,则运行环境终止,java程序退出。
异常类的继承体系
java常见异常类之间的继承体系:
从上图看出,java把所有的非正常情况分为两种:异常(Exception)和错误(Error)他们都继承Throwable父类
Error错误,一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误等,这种错误无法恢复或者不可能捕获,将导致应用程序中断。
//实例
public class DivTest {
public static void main(String[] args) {
try{
int a=Integer.parseInt(args[0]);
int b=Integer.parseInt(args[1]);
int c=a/b;
System.out.println(c);
}catch (IndexOutOfBoundsException ie){
System.out.println("数组越界:运行程序输入参数个数不够");
}catch (NumberFormatException ne){
System.out.println("数字格式异常:程序只能接受整数");
}catch (ArithmeticException ae){
System.out.println("算术异常");
}catch (Exception e){
System.out.println("未知异常");
}
}
}
异常捕获时,先捕获小异常,在捕获大异常
java7提供的多异常捕获
java7开始可以捕获多种类型的异常,需要注意的点:
- 捕获多种类型异常时,多种异常类型之间用(|)竖线隔开
- 捕获多种类型异常时,异常变量有隐式d额final修饰,因此程序不能对异常常量重新赋值
public class DivTest {
public static void main(String[] args) {
try{
int a=Integer.parseInt(args[0]);
int b=Integer.parseInt(args[1]);
int c=a/b;
System.out.println(c);
}catch (IndexOutOfBoundsException|NumberFormatException|ArithmeticException ie){
System.out.println("系统发生数组越界,数字格式异常,算术异常");
//捕获多个异常时,异常变量默认有final修饰
//所以下面代码报错
ie=new ArithmeticException("test");
}catch (Exception e){
System.out.println("未知异常");
//捕获一种异常时,异常变量没有final修饰
//所以下面代码正确
e=new Exception();
}
}
}
捕获多种类型的异常时,异常变量使用隐式的final修饰;捕获一种类型的异常时,异常变量没有final修饰
访问异常信息
程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch块后的异常形参来获得。当java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序可以通过该参数获取异常对象的信息。
异常对象的常用方法:
//演示
public class AccessExceptionMsg {
public static void main(String[] args) {
try{
FileInputStream fis=new FileInputStream("a.txt");
}catch (IOException ioe){
System.out.println(ioe.getMessage());
ioe.printStackTrace();
}
}
}
结果:
使用finally回收资源
程序在try块里打开了一些物理资源,这些物理资源都必须显式回收。
java的垃圾回收机制不会回收任何的物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。
为了保证一定回收try块中打开的物理资源,异常处理机制提供了finally块。
//语法结构
try{
//业务实现代码
}catch(SubExecption e){
//异常处理块1
}catch(SubExecption2 e){
//异常处理块2
}finally{
//资源回收块
}
注意:
- 尽量避免在finally块里使用return或throw等导致方法终止的语句,否则可能出现一些很奇怪的情况。
- 如果在异常处理代码中使用System.exit(1);语句来退出虚拟机,则finally块将会失去执行的机会
异常处理的嵌套
异常处理流程代码可以放在任何能放可执行代码的地方,因此完整的异常处理流程既放在try块里,也可以放在catch块里,还可以放在finally块里
异常处理嵌套的深度没有明确的限制,但通常没有必要使用超过两层的嵌套异常处理,层次太深的嵌套异常处理没有太大的必要,而且导致程序可读性降低。
java7的自动关闭资源的try语句
java7增强了try语句的功能,允许在try关键字后面紧跟一对圆括号,圆括号可以声明,初始化一个或多个资源,此处指的资源是那些必须在程序结束时显式关闭的资源(如数据库连接,网络连接),try语句在该语句结束时自动关闭这些资源
为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口,实现这两个接口中的close()方法
public class AutoCloseTest {
public static void main(String[] args)throws IOException {
try(
//声明,初始化两个可关闭的资源
//try语句会自动关闭着两个资源
BufferedReader br=new BufferedReader(
new FileReader("AutoClaseTest.java"));
PrintStream ps=new PrintStream(new FileOutputStream("a.txt"))) {
//使用两个资源
System.out.println(br.readLine());
ps.println("庄胜。。。。");
}
}
}
自动关闭资源try语句相当于包含了隐式的finally块(这个finally块用于关闭资源),因此这个try语句既可以没有catch块,也没有finally块
Checked异常和Runtime异常体系
java的异常被分为两大类:Checked异常和Runtime异常。
Checked异常:所有RuntimeException类及其子类的异常实例被称为Checked异常
Runtime异常:所有RuntimeException类及其子类的实例被称为Runtime异常
只有java提供Checked异常,其他语言都没有提供,java认为Checker异常都是可以被处理(修复)的异常,程序必须显式的处理Checked异常,否则程序在编译时就会发生错误,无法通过编译
Checked异常的处理方式:
- 当前方法明确知道如何处理该异常,程序应该使用try…catch块来捕获该异常,然后在对应的catch块中修复该异常
- 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出异常
Runtime异常处理方法
Runtime异常无须显式声明抛出,程序需要捕获Runtime异常,也可以使用try…catch块来实现
使用throws声明抛出异常
使用思路:当方法不知道如何处理这个类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常交给JVM处理。JVM对异常处理的方法是,打印异常的跟踪栈信息,并终止程序运行。
throws声明抛出异常只能在方法签名中使用,throws可以抛出多个异常类,多个异常类之间以逗号隔开
//格式
throws ExecptionClass1,ExecptionClass2.....
注意:
- 一旦使用throws语句声明抛出该异常,程序无须使用try…catch块来捕获该异常
- 如果某段代码调用throws声明的方法,该方法抛出了Checked异常,则表明该方法希望调用者来处理异常,也就是说,调用该方法要么放进try块中,要么放入另一个带throws声明的方法中
- throws声明抛出异常有一个限制,就是方法重写时“两小”中的一条规则:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多
public class OverrideThrows {
public void test()throws IOException {
FileInputStream fis=new FileInputStream("a.txt");
}
}
class Sub extends OverrideThrows{
//子类方法声明抛出了比父类方法更大的异常
//所以下面错误
public void test()throws Exception{
}
}
使用Checked异常至少存在两大不便:
- 对于程序中Checked异常,java要求必须显式捕获并处理该异常,或者显式的声明抛出该异常。这就增加了编程复杂度
- 如果在方法中显式声明抛出Checked异常,将会导致方法签名与异常耦合,如果该方法是重写父类的方法,则该方法抛出的异常还会受到被重写方法所抛出异常的限制
使用throw抛出异常
当程序出现错误时,系统会自动抛出异常;除此之外,java也允许程序自行抛出异常,自行抛出异常使用throw语句完成。
抛出异常
如果需要在程序中自行抛出异常,则应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。
注意:
- 如果throw语句抛出的异常是Checked异常,则该throw语句要么处于try块里,显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给该方法的调用者处理;
- 如果throw语句抛出的异常是Runtime异常,则该语句无须放在try块里,也无须放在带throws声明抛出异常的方法中;程序既可以显式使用try…catch来捕获并处理该异常,也可以完全不理会该异常,把异常交给调用者处理
public class ThrowTest {
public static void main(String[] args) {
try{
//调用声明抛出Checked异常方法,要么显式捕获异常
//要么在mian方法中再次声明抛出
throwChecked(-3);
}catch (Exception e){
System.out.println(e.getMessage());
}
//调用声明抛出Runtime异常的方法即可以显式捕获该异常
//也可以不理会该异常
throwRuntime(3);
}
public static void throwChecked(int a)throws Exception{
if(a>0){
//自行抛出异常
//该代码必须处于try块里,或处于待throws声明的方法中
throw new Exception("a的值大于0,不符合要求");
}
}
public static void throwRuntime(int a){
if(a>0){
//自行抛出RuntimeException异常,即可以显式捕获异常
//也可以完全不理会该异常,把该异常交给该方法调用者处理
throw new RuntimeException("a的值大于0,不符合要求");
}
}
}
自定义异常类
用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参数构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信心(也就是异常对象的getMessage()方法的返回值)
//自定义异常类实例
public class AuctionException extends Exception{
//无参数的构造器
public AuctionException(){}
//带一个字符串参数的构造器
public AuctionException(String msg){
super(msg);
}
}
catch和throw同时使用
在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。
为了实现这种通过多个方法协作 处理同一个异常的情形,可以在catch块中结合throw语句来完成
//实例
public class AuctionTest {
private double initPrice=30.0;
//因为该方法中显式抛出了AuctionException异常
//所以此处需要声明抛出AuctionException异常
public void bid(String bidPrice)throws AuctionException{
double d=0.0;
try{
d=Double.parseDouble(bidPrice);
}catch (Exception e){
//此处完成本方法中可以对异常执行的修复处理
//此处仅仅是在控制台打印异常的跟踪栈信息
e.printStackTrace();
//再次使用自定义异常
throw new AuctionException("竞拍价必须是数值"+"不能包含其他字符");
}
if(initPrice>d){
throw new AuctionException("竞拍价比起拍价低,不允许竞拍");
}
initPrice=d;
}
public static void main(String[] args) {
AuctionTest at=new AuctionTest();
try{
at.bid("df");
}catch (AuctionException ae){
//再次捕获到bind()方法中的异常,并对该异常进行处理
System.out.println(ae.getMessage());
}
}
}
java7增强的throw语句
异常链
对于真实的企业级应用而言,常常有严格的分层关系,层与层之间有非常清晰的划分,上层功能的实现严格依赖于下层的API,也不会跨层访问。
如图
当业务逻辑访问持久层出现异常时,程序不会把底层的异常传到用户界面,有两个原有:
- 对于用户而已,他们不想看到底层的异常,这些异常对他们使用该系统没有任何帮助
- 对于恶意用户而言,将异常暴露出来不安全
把底层的原始异常直接传给用户是一种不负责任的表现。通常的做法:程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为异常转译。
把捕获一个异常然后接着抛出另外一个异常,并把原始异常信息保存下来是一种典型d额链式处理(23种设计模式之一,职责链模式),被称为异常链
java的异常跟踪栈
异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,开发者可以找到异常的开头,并跟踪到异常一路触发的过程。
//示例
class SelfException extends RuntimeException{
SelfException(){}
SelfException(String msg){
super(msg);
}
}
public class PrintStackTraceTest {
public static void main(String[] args) {
firstMethod();
}
public static void firstMethod(){
secondMethed();
}
public static void secondMethed(){
thirdMethod();
}
public static void thirdMethod(){
throw new SelfException("自定义异常");
}
}
通过上图可以看出java的异常追踪。
异常处理规则
成功的异常处理应该实现如下4个目标
- 使程序代码混乱最小化
- 捕获保留诊断信息
- 通知合适的人员
- 才用合适的方式结束异常活动
不要过度使用异常
过度使用异常主要有两方面
- 把异常和普通错误混淆在一起,不在编写任何错误处理代码,而是以简单的抛出异常来代替所有的错误代码
- 使用异常处理来代替流程控制
不要使用过于庞大的try块
避免使用Catch All语句
不要忽略捕获到的异常
异常追踪。
异常处理规则
成功的异常处理应该实现如下4个目标
- 使程序代码混乱最小化
- 捕获保留诊断信息
- 通知合适的人员
- 才用合适的方式结束异常活动
不要过度使用异常
过度使用异常主要有两方面
- 把异常和普通错误混淆在一起,不在编写任何错误处理代码,而是以简单的抛出异常来代替所有的错误代码
- 使用异常处理来代替流程控制