Java的异常机制主要依赖于try、catch、finally、throw和throws五个关键字。
Java将异常分为两种,Checked异常和Runtime异常,Java认为Checked异常都是可以在运行期间得到解决的异常,所以它强制要求程序处理所有的Checked异常;而Runtime异常则无须处理。
异常处理机制:
Java异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情形时,系统会自动生成一个Exception对象来通知程序,从而实现将“业务功能实现代码”
和“错误处理代码”分离,提供更好的可读性。
使用try...catch捕获异常:
1 //Java异常处理机制的语法结构 2 try{ 3 //业务实现 4 ... 5 }catch (Exception e){ 6 alert 输入不合法 7 goto retry 8 }
若执行try块中的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象提交给Java运行时环境,这个过程被称为抛出(throw)异常。
当Java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,若找到合适的catch块,则把该异常对象交给该catch块处理,这个过程被称为捕获(catch)异常;
若Java运行时环境找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出。
1 try{ 2 //将用户输入的字符串以逗号(,)作为分隔符,分割成2个字符串 3 String[] posStrArr = inputStr.split(","); 4 //将2个字符串转换成用户下棋的坐标 5 int xPos = Integer.parseInt(posStrArr[0]); 6 int yPos = Integer.parseInt(posStrArr[1]); 7 //把对应的数组元素赋为“O” 8 if(!gb.board[xPos - 1][yPos - 1].equals("+")){ 9 System.out.println("您输入的坐标点已有棋子了," + "请重新输入"); 10 continue; 11 } 12 gb.board[yPos - 1][xPos - 1] = "O"; 13 }catch (Exception e){ 14 System.out.println("您输入的坐标不合法,请重新输入," + "下棋坐标应以x,y的格式"); 15 continue; 16 }
上面代码把处理用户输入字符串的代码都放在try块里进行,只要用户输入的字符串不是有效坐标值(包括字母不能正确解析,没有逗号不能正确解析,解析出来的坐标引
起数组越界...),系统都将抛出异常对象,并把这个异常交个对应的catch块。
异常类的继承体系:
当Java运行时环境接收到异常对象时,如何为该异常对象寻找catch块?上面程序中catch关键字的形式:(Exception e),这意味着每个catch块都是专门用于处理该异
常类及其子类的异常实例。
当Java运行时环境接收到异常对象后,会依次判断该异常对象是否是catch块后异常类或其子类的实例,若是,Java运行时环境将调用该catch块来处理该异常;否则再次
拿该异常对象和下一个catch块里的异常类进行比较。
try块后面的花括号{ }不可省略,即使try块中只有一条代码,也不可省略这个花括号。catch块后的花括号也不可被省略。在try块中声明的变量是代码块内局部变量,只在try
块内有效,在catch块中不能访问该变量。
Java常见异常类之间的继承关系图:
从图中可看出,Java把所有的非正常情况分成两种:异常(Exception)和错误(Error),它们都继承Throwable父类。
Error错误一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,动态链接失败等,这种错误无法恢复或不可捕获,将导致应用程序中断。通常应用程序无法处理这些错误,
因此应用程序不应该试图使用catch块来捕获Error对象,也无需在其throws子句中声明该方法可能抛出Error及其任何子类。
1 public class DivTest{ 2 public static void main(String[] args){ 3 try{ 4 int a = Integer.parseInt(args[0]); 5 int b = Integer.parseInt(args[1]); 6 int c = a / b; 7 System.out.println("您输入的两个数相除的结果是:" + c); 8 }catch(IndexOutOfBoundsException ie){ 9 System.out.println("数组越界:运行程序时输入的参数个数不够"); 10 }catch(NumberFormatException ne){ 11 System.out.println("数字格式异常:程序只能接收整数参数"); 12 }catch(ArithmeticException ae){ 13 System.out.println("算术异常"); 14 }catch(Exception e){ 15 System.out.println("未知异常"); 16 } 17 } 18 }
IndexOutOfBoundsException:索引越界异常
NumberFormatException:数字格式异常
ArihmeticException:算术异常,计算的表达式无意义
1 import java.util.Date; 2 3 public class NullTest{ 4 public static void main(String[] args){ 5 Date d = null; 6 try{ 7 System.out.println(d.after(new Date())); 8 }catch(NullPointerException ne){ 9 System.out.println("空指针异常"); 10 }catch(Exception e){ 11 System.out.println("未知异常"); 12 } 13 } 14 }
NullPointerException:当试图调用一个null对象的实例方法或实例变量时,就会引发NullPointerException异常
捕获异常时注意,一定先捕获小异常,再捕获大异常。
Java7 提供的多异常捕获:
Java7以前,每个catch块只能捕获一种类型的异常;但从Java7开始,一个catch块可以捕获多种类型的异常。
使用一个catch块捕获多种类型的异常时需要注意如下两个地方:
1.捕获多种类型的异常时,多种异常类型之间需要用(|)竖线隔开
2.捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值。
1 public class MultiExceptionTest{ 2 public static void main(String[] args){ 3 try{ 4 int a = Integer.parseInt(args[0]); 5 int b = Integer.parseInt(args[1]); 6 int c = a / b; 7 System.out.println("您输入的两个数相除的结果是:" + c); 8 }catch(IndexOutOfBoundsException | NumberFormatException | ArithmeticException ie){ 9 System.out.println("程序放生了索引越界,数字格式异常、算术异常之一"); 10 //捕获多异常时,异常变量默认有final修饰 11 //所以下面代码有错 12 ie = new ArithmeticException("test"); 13 }catch(Exception e){ 14 System.out.println("未知异常"); 15 //捕获一种类型的异常时,异常变量没有final修饰 16 //所以下面代码完全正确 17 e = new RuntimeException("test"); 18 } 19 } 20 }
捕获多种类型异常时,异常变量使用隐式的final修饰,因此产生编译错误;捕获一种异常时,异常变量没有final修饰,因此不会出错。
访问异常信息:
若程序需要在catch块中访问异常对象的相关信息,可通过访问catch块后面的异常形参来获得。
当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息:
1.getMessage():返回该异常的详细描述字符串
2.printStackTrace():将该异常的跟踪栈信息输出到标准错误输出
3.printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流
4.getStackTrace():返回该异常的跟踪栈信息
1 import java.io.FileInputStream; 2 import java.io.IOException; 3 4 public class AccessExceptionMsg{ 5 public static void main(String[] args){ 6 try{ 7 FileInputStream fis = new FileInputStream("a.txt"); 8 }catch(IOException ioe){ 9 System.out.println(ioe.getMessage()); 10 ioe.printStackTrace(); 11 } 12 } 13 }
“a.txt”(系统找不到指定的文件):是调用异常的getMessage()方法返回的字符串。下面更详细的信息是该异常的跟踪栈信息。
使用finally回收资源:
有些时候,程序在try块中打开了一些物理资源(如:数据库连接,网络连接,磁盘文件等),这些物理资源都必须显示回收。
Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。
为保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了
return语句,finally块总是被执行。完整的Java异常处理语法结构如下:
1 //完整的Java异常处理语法结构 2 try{ 3 //业务实现 4 ... 5 }catch (SubException e){ 6 //异常处理块1 7 ... 8 }catch(SubException2 e){ 9 //异常处理块2 10 ... 11 } 12 ... 13 finally{ 14 //资源回收 15 ... 16 }
异常处理语法结构中只有try块是必须的,即若没有try块,则不能有后面的catch块和finally块;
catch块和finally块都是可选的,但catch块和finally块至少出现其中之一,也可以同时出现;
可以有多个catch块,捕获父类异常的catch块必须位于捕获子类异常的后面;
但不能只有try块,既没有catch块,也没有finally块;
多个catch块必须位于try块之后,finally块必须位于所有的catch块之后。
1 import java.io.FileInputStream; 2 import java.io.IOException; 3 4 public class FinallyTest{ 5 public static void main(String[] args){ 6 FileInputStream fis = null; 7 try{ 8 fis = new FileInputStream("a.text"); 9 }catch(IOException ioe){ 10 System.out.println(ioe.getMessage()); 11 //return语句强制方法返回 12 return ; 13 //使用exit退出虚拟机 14 //System.exit(1); 15 }finally{ 16 //关闭磁盘文件,回收资源 17 if(fis != null){ 18 try{ 19 fis.close(); 20 }catch(IOException ioe){ 21 ioe.printStackTrace(); 22 } 23 } 24 System.out.println("执行finally块里的资源回收!"); 25 } 26 } 27 }
finally块被执行了,将上面代码的return ;注释掉,将System.exit(1);打开:
上面结果表明finally块没有被执行。若在异常处理代码中使用System.exit(1)语句退出虚拟机,则finally块将失去执行的机会。
除非在try块、catch块中调用了退出虚拟机的方法,否则不管在try块,catch块中执行了怎样的代码,出现怎样的情况,异常处理的finally块总会被执行。
通常情况下,不要再finally块中使用return或throw等导致方法终止的语句,(throws语句将在后面介绍),一旦在finally块中使用了return或throw语句,将会导致try块,
catch块中的return、throw语句失效:
1 public class FinallyFlowTest{ 2 public static void main(String[] args) throws Exception { 3 boolean a = test(); 4 System.out.println(a); 5 } 6 7 public static boolean test(){ 8 try{ 9 //因为finally块中包含了return语句 10 //所以下面的return语句失去了作用 11 return true; 12 }finally{ 13 return false; 14 } 15 } 16 }
上面程序中,try块中的return true;语句失去了作用。
当Java程序执行try块、catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流
程中是否包含finally块,若没有finally块,程序立即执行return或throw语句,该方法终止;若有finally块,系统立即开始执行finally——只有当finally块执行完成后,系统
才会再次跳回来执行try块、catch块里的return或throw语句;若finally块里也使用了return或throw等导致方法终止语句,finally块已经终止了方法,系统将不会跳回去执行
try块、catch块中的任何代码。
尽量避免在finally块中使用return或throw等导致方法终止的语句。
异常处理的嵌套:
1 import java.io.FileInputStream; 2 import java.io.IOException; 3 4 public class FinallyTest{ 5 public static void main(String[] args){ 6 FileInputStream fis = null; 7 try{ 8 fis = new FileInputStream("a.text"); 9 }catch(IOException ioe){ 10 System.out.println(ioe.getMessage()); 11 //return语句强制方法返回 12 //return ; 13 //使用exit退出虚拟机 14 System.exit(1); 15 }finally{ 16 //关闭磁盘文件,回收资源 17 if(fis != null){ 18 try{ 19 fis.close(); 20 }catch(IOException ioe){ 21 ioe.printStackTrace(); 22 } 23 } 24 System.out.println("执行finally块里的资源回收!"); 25 } 26 } 27 }
正如上面程序所示,finally块中也包含了一个完整的异常处理流程,这种在try块、catch块或finally块中包含完整的异常处理流程的情况被称为异常处理的嵌套。
异常处理嵌套的深度没有明确限制,通常,没有必要使用超过两层的嵌套异常处理。
Java7的自动关闭资源的try语句:
Java7增强了try语句的功能——它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显示关闭的资源
(比如:数据库连接、网络连接等),try语句在该语句结束时自动关闭这些资源。
需要指出的是:为保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口,实现这两个接口就必须实现close()方法。
Closeable是AutoCloseable的子接口,可以被自动关闭的资源类要么实现AutoCloseable接口,要么实现Closeable接口。
Closeable接口里的close方法声明抛出了IOException,,因此它的实现类在实现close()方法时只能声明抛出IOException或其子类;
AutoCloseable接口里的close()方法声明抛出了Exception,因此它的实现类在实现close()方法时可以声明抛出任何异常。
1 import java.io.BufferedReader; 2 import java.io.FileReader; 3 import java.io.FileOutputStream; 4 import java.io.PrintStream; 5 import java.io.IOException; 6 7 public class AutoCloseTest{ 8 public static void main(String[] args) throws IOException{ 9 try( 10 //声明、初始化两个可关闭的资源 11 //try语句会自动关闭这两个资源 12 BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest.java")); 13 PrintStream ps = new PrintStream(new FileOutputStream("a.txt")); 14 ){ 15 //使用两个资源 16 System.out.println(br.readLine()); 17 ps.println("庄生晓梦迷蝴蝶"); 18 } 19 } 20 }
这里提醒一句,try()圆括号中的最后一条语句的分号,可要可不要。
上面程序中try语句中声明、初始化了BufferedReader、PrintStream,所以try语句会自动关闭它们。
自动关闭资源的try语句相当于包含了隐式的finally块,因此这个try语句可以既没有catch块,也没有finally块。
Java7几乎把所有的“资源类”(包括文件IO的各种类,JDBC编程的Connection、Statement等接口)进行了改写,改写后的资源类都实现了AutoCloseable或Closeable接口
若程序需要,自动关闭资源的try语句后也可带多个catch块和一个finally块。
Checked异常和Runtime异常体系:
Java异常分为两大类:Checked异常和Runtime异常(运行时异常)。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类
的异常实例被称为Checked异常。
Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显示处理Checked异常,若程序没有处理Checked异常,改程序会在编译时报错,无法通过编译
使用throws声明抛出异常:
当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;若main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常交给
JVM处理。JVM对异常的处理方法是,打印异常的跟踪栈信息,并终止程序运行,这就是前面程序在遇到异常后自动结束的原因。
throws声明抛出只能在方法签名中使用,throws可以声明抛出多个异常类,多个异常类间以逗号隔开:
throws ExceptionClass1, ExceptionClass2...
上面throws声明抛出的语法格式仅跟在方法签名之后,一旦使用throws语句声明抛出该异常,程序就无需使用try...catch块来捕获该异常了。
1 import java.io.FileInputStream; 2 import java.io.IOException; 3 4 public class ThrowsTest{ 5 public static void main(String[] args) throws IOException{ 6 FileInputStream fis = new FileInputStream("a.txt"); 7 } 8 }
上面程序声明不处理IOException异常,将该异常交给JVM处理,所以一旦程序遇到该异常,JVM就会打印该异常的跟踪栈信息,并结束程序。
若某段代码中调用了一个带throws声明的方法,该方法声明抛出了Checked异常,则表明该方法希望它的调用者来处理该异常。
处理异常的方法有两种,一是调用该方法是放在try块中显示捕获该异常,二是在另一个带throws声明抛出的方法中,交给上一级处理:
1 import java.io.FileInputStream; 2 import java.io.IOException; 3 4 public class ThrowsTest2{ 5 public static void main(String[] args) throws Exception{ 6 //因为test()方法声明抛出IOException异常 7 //所以调用该方法的代码要么处于try...catch块中 8 //要么处于另一个带throws声明抛出的方法中 9 test(); 10 } 11 12 public static void test() throws IOException{ 13 //因为FileInputStream的构造器声明抛出IOException异常 14 //所以调用FileInputStream的代码要么处于try...catch块中 15 //要么处于另一个带throws声明抛出的方法中 16 FileInputStream fis = new FileInputStream("a.txt"); 17 } 18 }
使用throws声明抛出异常时有一个限制,就是方法重写时“两小”中的一条规则:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子
类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
1 import java.io.FileInputStream; 2 import java.io.IOException; 3 4 public class OverrideThrows{ 5 public void test() throws IOException{ 6 FileInputStream fis = new FileInputStream("a.txt"); 7 } 8 } 9 class Sub extends OverrideThrows{ 10 //子类方法声明抛出了比父类方法更大的异常 11 //所以下面方法出错 12 public void test() throws Exception{ } 13 }
上面程序中Sub子类中的test()方法声明抛出的异常Exception比父类声明抛出异常IOException要大,所以将导致无法通过编译。
由此可见,使用Checked异常至少存在如下两大不便之处:
1.对于程序中的Checked异常,java要求必须显示捕获并处理异常,或显示声明抛出该异常,增加了编程复杂度
2.若在方法中显示声明抛出Checked异常,将会导致方法签名与异常耦合,若该方法是重写父类的方法,则该方法抛出的异常还会受到被重写方法所抛出异常的限制
大部分推荐使用Runtime异常,而不是使用Checked异常,尤其当程序需要自行抛出异常时(如何自行抛出异常下面会讲),使用Runtime异常将更加简洁。
使用throw抛出异常:
当程序出现错误是,系统会自动抛出异常;除此之外,Java也允许程序自行抛出异常,自行抛出异常使用throw语句来完成(注意此处的throw没有后面的s,与前面声明
抛出的throws是有区别的)。
抛出异常:
若需要在程序中自行抛出异常,则应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是异常实例,且每次只能抛出一个异常实例。
格式:
throw ExceptionInstance;
改写五子棋游戏处理用户输入的代码:
1 try{ 2 //将用户输入的字符串以逗号(,)作为分隔符,分割成2个字符串 3 String[] posStrArr = inputStr.split(","); 4 //将2个字符串转换成用户下棋的坐标 5 int xPos = Integer.parseInt(posStrArr[0]); 6 int yPos = Integer.parseInt(posStrArr[1]); 7 //把对应的数组元素赋为“O” 8 if(!gb.board[xPos - 1][yPos - 1].equals("+")){ 9 throw new Exception("您输入的坐标点已有棋子了," + "请重新输入"); 10 } 11 gb.board[yPos - 1][xPos - 1] = "O"; 12 }catch (Exception e){ 13 System.out.println("您输入的坐标不合法,请重新输入," + "下棋坐标应以x,y的格式"); 14 continue; 15 }
上面程序用throw语句来自行抛出异常,程序认为当用户试图想一个已有棋子的坐标点下棋就是异常。当Java运行时接收到异常时,会终止当前执行流,跳到该异常对应
的catch块,由catch来处理该异常。
若throw抛出的异常是Checked异常,则该throw语句要么处于try块中,显示捕获异常,要么放在一个带throws声明抛出的方法中,即把该异常交给该方法的调用者处理;
若throw语句抛出的异常时Runtime异常,则该语句无需放在try块里,也无需放在带throws声明抛出的方法中;程序既可以显示使用try...catch来捕获并处理该异常,也可
以完全不理会该异常,把该异常交给该方法调用者来处理。
1 public class ThrowTest{ 2 public static void main(String[] args){ 3 try{ 4 //调用声明抛出checked异常的方法,要么显示捕获该异常 5 //要么在main方法中再次声明抛出 6 throwChecked(-3); 7 }catch(Exception e){ 8 System.out.println(e.getMessage()); 9 } 10 //调用声明抛出Runtime异常的方法既可以显示捕获该异常 11 //也可以不理会该异常 12 throwRunTime(3); 13 } 14 15 public static void throwChecked(int a) throws Exception{ 16 if(a > 0){ 17 //自行抛出Exception 18 //该代码必须处于try块里,或处于带throws声明的方法中 19 throw new Exception("a的值大于0,不符合要求"); 20 } 21 } 22 23 public static void throwRunTime(int a){ 24 if(a > 0){ 25 //自行抛出RuntimeException异常,既可以显示捕获该异常 26 //也可完全不理会该异常,把该异常交给该方法调用者处理 27 throw new RuntimeException("a的值大于0,不符合要求"); 28 } 29 } 30 }
自定义异常类:
通常情况下,程序很少会自行抛出系统异常,因为异常的类名通常也包含了该异常的有用信息,所以在选择抛出异常时,应该选择合适的异常类,从而可以明确的描述该
异常情况。在这种情况下,应用程序常需要抛出自定义异常。
用户自定义异常都应该继承Exception基类,若希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:
1.无参数的构造器
2.带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的getMessage()方法的返回值)。
1 public class AuctionException extends Exception{ 2 //无参数的构造器 3 public AuctionException(){} 4 //带一个字符串参数的构造器 5 public AuctionException(String msg){ 6 super(msg); 7 } 8 }
上面程序创建了一个AuctionException异常类,并为该异常类提供了两个构造器,带参构造器,仅通过调用父类的构造器,正是这行super调用可以将此字符串参数传给异
常对象的message属性,该message属性就是该异常对象的详细描述信息。
若需要自定义Runtime异常,只需要将上面程序中的Exception基类改为RuntimeException基类,其他地方无需更改。
大部分情况下,创建自定义异常都可采用与上面程序相似的代码完成,只需改变上面代码异常的类型即可,让该异常类的类名可以准确描述该异常。
catch和throw同时使用:
前面介绍的异常处理方式有如下两种:
1.在出现异常的方法内捕获并处理异常,该方法的调用者将不能再次捕获该异常
2.该方法签名中声明抛出该异常,将该异常完全交给方法调用者处理
实际应用中旺旺需要更复杂的处理方式——当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理该异常。即:在异常出现的当前方
法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。
为实现这种通过多个方法协作处理同一个异常的情形,可在catch块中结合throw语句来完成:
1 public class AuctionException extends Exception{ 2 //无参数的构造器 3 public AuctionException(){} 4 //带一个字符串参数的构造器 5 public AuctionException(String msg){ 6 super(msg); 7 } 8 }
1 public class AuctionTest{ 2 private double initPrice = 30.0; 3 //因为该方法中显式抛出了AuctionException异常 4 //所以此处需要声明抛出AuctionException异常 5 public void bid(String bidPrice) throws AuctionException{ 6 double d = 0.0; 7 try{ 8 d = Double.parseDouble(bidPrice); 9 }catch(Exception e){ 10 //此处完成本方法中可以对异常执行的修复处理 11 //此处仅仅是在控制台打印异常的跟踪栈信息 12 e.printStackTrace(); 13 //再次抛出自定义异常 14 throw new AuctionException("竞拍价必须是数值," + "不能包含其他字符!"); 15 } 16 17 if(initPrice > d){ 18 throw new AuctionException("竞拍价比起拍价底," + "不允许竞拍!"); 19 } 20 21 initPrice = d; 22 } 23 24 public static void main(String[] args){ 25 AuctionTest at = new AuctionTest(); 26 try{ 27 at.bid("df"); 28 }catch(AuctionException ae){ 29 //再次捕获到bid()方法中的异常,并对该异常进行处理 30 System.out.println(ae.getMessage()); 31 } 32 } 33 }
上面代码在bid()方法中catch块捕获到异常后,系统打印出该异常的跟踪栈信息,接着抛出一个AuctionException异常,通知该方法的调用者再次处理该AuctionException
异常。所以程序中的main()方法,也就是bid()方法调用者还可以再次捕获AuctionException异常,并将该异常的详细信息输出到标准错误输出。
这种catch和throw结合使用的情况在大型企业级应用中非常常用。企业级应用对异常的处理通常分为两部分:
1.应用后台需要通过日志来记录异常发生的详细情况;
2.应用还需要根据异常向应用使用者传达某种提示。
在这种情况下所有异常都需要两个方法共同完成,也就必须将catch和throw结合使用。
Java7增强的throw语句:
看下面代码:
1 try{ 2 new FileOutputStream("a.txt"); 3 }catch(Exception ex){ 4 ex.printStackTrace(); 5 throw ex; 6 }
上面代码,throw再次抛出异常,但这个ex对象的情况比较特殊:程序捕获该异常时,声明该异常的类型为Exception;但实际上try块中只调用了FileOutputStream构
造器,这个构造器声明只是抛出了FileNotFoundException异常。
Java7之前,由于在捕获该异常声明ex的类型是Exception,因此Java编译器认为这段代码可能抛出Exception异常,所以包含这段代码的方法通常需要声明抛出Exception
异常,如下:
1 public class ThrowTest2{ 2 public static void main(String[] args) 3 //Java6认为下面方法代码可能抛出Exception异常 4 //所以此处声明抛出Exception异常 5 throws Exception{ 6 try{ 7 new FileOutputStream("a.txt"); 8 }catch(Exception ex){ 9 ex.printStackTrace(); 10 throw ex; 11 } 12 } 13 }
从Java7后,Java编译器会执行更细致的检查,Java编译器会检查throw语句抛出的异常的实际类型,这样编译器知道throw实际上只可能抛出FileNotFoundException异常,
因此在方法签名中只要声明抛出FileNotFoundException异常即可:
1 public class ThrowTest2{ 2 public static void main(String[] args) 3 //Java6认为下面方法代码可能抛出Exception异常 4 //所以此处声明抛出Exception异常 5 throws FileNotFoundException{ 6 try{ 7 new FileOutputStream("a.txt"); 8 }catch(Exception ex){ 9 ex.printStackTrace(); 10 throw ex; 11 } 12 } 13 }
异常链:
把底层如SQLException异常传给用户是一种不负责任的表现。通常做法是:
程序捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为异常转译。
假设程序需要实现工资计算的方法,则程序采用如下结构的代码来实现该方法:
1 public calSal() throws SalException{ 2 try{ 3 //实现结算工资的业务逻辑 4 ... 5 }catch (SQLException sqle){ 6 //把原始异常记录下来,留给管理员 7 ... 8 //下面异常中的message就是对用户的提示 9 throw new SalException("访问底层数据库出现异常"); 10 }catch(Exception e){ 11 //把原始异常记录下来,留给管理员 12 ... 13 //下面异常中的message就是对用户提示 14 throw new SalException("系统出现未知异常"); 15 } 16 }
这种把原始异常信息隐藏起来,仅向上提供必要的异常提示信息的处理方式,可以保证底层异常不会扩散到表现层,可以避免向上暴露太多的实现细节。
这种捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理(23种设计模式之一:职责链模式),也被称为“异常链”。
Java1.4前,程序员需自己编写代码来保存原始异常信息。Java1.4后,所有Throwable的子类在构造器中都可以接受一个cause对象作为参数。这个cause就用来表示原始异常,
这样就可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出的新异常,也能通过这个异常链追踪到异常最初发生的位置。如希望通过上面的SalException去追
踪到最原始的异常信息,可以将方法改写为如下:
1 public calSal() throws SalException{ 2 try{ 3 //实现结算工资的业务逻辑 4 ... 5 }catch (SQLException sqle){ 6 //把原始异常记录下来,留给管理员 7 ... 8 //下面异常中的sqle就是原始异常 9 throw new SalException(sqle); 10 }catch(Exception e){ 11 //把原始异常记录下来,留给管理员 12 ... 13 //下面异常中的e就是原始异常 14 throw new SalException(e); 15 } 16 }
上面代码创建SalException对象时,传入一个Exception对象,这就需要SalException类要有相应的构造器,Java1.4之后,Throwable基类已有了一个可以接收Exception
参数的方法,所以可以采用如下代码定义SalException类:
1 public class SalException extends Exception{ 2 public SalException(){} 3 public SalException(String msg){ 4 super(msg); 5 } 6 7 //创建一个可以接收Throwable参数的构造器 8 public SalException(Throwable t){ 9 super(t); 10 } 11 }
创建这个SalException业务异常类后,就可以用它来封装原始异常,从而实现异常的链式处理。
Java的异常跟踪栈:
异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据该方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程
1 class SelfException extends RuntimeException{ 2 SelfException(){} 3 SelfException(String msg){ 4 super(msg); 5 } 6 } 7 8 public class PrintStackTraceTest{ 9 public static void main(String[] args){ 10 firstMethod(); 11 } 12 public static void firstMethod(){ 13 secondMethod(); 14 } 15 public static void secondMethod(){ 16 thirdMethod(); 17 } 18 public static void thirdMethod(){ 19 throw new SelfException("自定义异常信息"); 20 } 21 }
下面例子示范了多线程程序中发生异常的情形:
1 public class ThreadExceptionTest implements Runnable{ 2 public void run(){ 3 firstMethod(); 4 } 5 public void firstMethod(){ 6 secondMethod(); 7 } 8 public void secondMethod(){ 9 int a = 5; 10 int b = 0; 11 int c = a / b; 12 } 13 14 public static void main(String[] args){ 15 new Thread(new ThreadExceptionTest()).start(); 16 } 17 }
虽然printStackTrace()方法很方便的用于追踪异常发生情况,可用它来调试程序,但在最后发布程序中,应避免使用它;而应该对捕获的异常进行适当的处理而不是简单
的将异常的跟踪栈信息打印出来。
异常处理规则:
1.使程序代码混乱最小化
2.捕获并保留诊断信息
3.通知合适的人员
4.采用合适的方式结束异常活动
不要过度使用异常,过度使用异常的两个方面:
1.把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单的抛出异常来代替所有的错误信息
2.使用异常处理来代替流程控制。
异常处理机制的初衷是:将不可预期异常的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来代替正常的业务逻辑判断。
不要使用过于庞大的try块:
try块中的代码应该越少越好。正确的做法是:把大块的try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常。
避免使用Catch All语句:
所谓Catch All语句指的是一种异常捕获模块,它可以处理程序发生的所有可能异常。
这种处理方式有两个缺点:
1.所有的一场都采用相同的处理方式,导致无法对不同的异常分情况处理,若要分情况处理,需在catch块中使用分支语句进行控制,这种方式得不偿失。
2.这种捕获方式可能将程序中的错误、Runtime异常等可能导致程序终止的情况全部捕获到,从而“压制”了异常。若出现了一些“关键”异常,那么此异常也会被忽略。
不要忽略捕获到的异常:
对捕获到的异常做适当处理:
1.处理异常,对异常进行合适的修复,然后绕过异常发生的地方继续执行;或者用别的数据进行计算;或者提示用户重新操作等,对Checked异常,程序尽量修复
2.重新抛出异常:把当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新抛给上层调用者。
3.在合适的层处理异常:若当前层不清楚如何处理异常,就不要在当前层使用catch语句来捕获该异常,直接使用throws声明抛出该异常,让上层调用者处理该异常。