Java程序设计11——异常处理
1 概述
异常机制已经成为判断一门编程语言是否成熟的标准,除了传统的像C语言没有提供异常机制之外,目前主流的编程语言如Java、Ruby、Python都提供了成熟的异常机制。异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加健壮、优雅。
Java的异常机制主要依赖于try、catch、finally、throw和throws五个关键字,其中try关键字后紧跟一个花括号括起来的代码块(花括号不可省略)简称为try块,它里面放置可能引发异常的代码(如果没有异常机制会被当成正常的代码)。catch后对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块。多个catch块后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行。throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常,而throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体异常对象。我们希望所有错误都可以在编译阶段被发现,就是在试图运行程序之前就能排除所有错误,但这是不现实的,余下的问题必须在运行期间得到解决。
Java将异常分为两种,Checked异常和Runtime异常,Java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有Checked异常,而Runtime异常无须处理。
首先我们需要明确异常处理的必要性。因为程序的运行环境不可能永远是正常,总有一些吊诡的情况,比如操作系统不稳定、硬件突然坏掉或者网络不通畅等等情况。其次我们需要在发生异常的时候能够予以友好的提示,不能发生了异常,程序没法使用,用户还不知道是什么原因,抛出一个友好提示的异常可以帮助更好、更快的解决问题。
2 Java异常处理机制
Java的异常处理可以让程序具有更好的容错性,程序更加健壮。当程序运行出现意外时,系统会自动生成一个Exception对象来通知程序,从而实现将业务功能实现代码和错误处理代码分离,提供更好的可读性。
2.1 使用try...catch捕获异常
考虑伪码
if(一切正常){ //业务实现代码 } else{ alert 输入不合法 goto retry }
上面的if块依然不可以表示————一切正常是很抽象的,无法转换为计算机可识别的代码。在这种情况下,Java提出了一种假设:如果程序可以顺利完成,那就一切正常,把系统的业务实现代码放在try块中定义,所有的异常处理逻辑放在catch块中进行处理,下面是Java异常处理机制。
try{ //业务实现代码 .... } catch(Exception e){ alert 输入不合法 goto retry }
如果执行try块里业务逻辑代码时出现异常,系统自动生成一个异常对象,该对象被提交给Java运行时环境,这个过程被称为throw异常。当Java运行时环境收到异常对象时,会寻找能处理该异常的catch块,如果恩呢该找到合适的catch块并把该异常对象交给catch块处理,那这个过程被称为捕获异常;如果Java运行时找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出。
其实java异常处理机制的核心思想就是:正常的情况是优先考虑的(异常的概率正常情况概率从人的认知角度考虑来说是小很多的),但是异常的情况也需要考虑,这就是异常处理的核心思想,而不是把所有异常全部都写上去,因为异常是无法全部考虑的。
2.2 异常类的继承体系
当Java运行时环境接收到异常对象时,catch关键字形式(Exception e)的每一个catch块都会处理该异常类及其实例。
当Java运行时环境接收到异常对象后,会依次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,Java运行时环境将调用该catch块来处理该异常;否则再次判断该异常对象和下一个catch块里的异常类进行比较。最终找到一个匹配的,如果还是没有找到,执行finally块。
异常捕获示意图:
当程序进入负责异常处理的catch块时,系统生成的异常对象ex将会传给catch块后的异常形参,从而允许catch块通过该对象来获得异常的详细信息。
从上图可以看出,try块后可以有多个catch块,try块后使用多个catch块是为了针对不同异常类提供不同的异常处理方式。当系统发生不同意外情况时,系统会生成不同的异常对象,Java运行时就会根据该异常对象所属的异常类来决定使用哪个catch块来处理该异常。
通过在try块后提供多个catch块可以无须在异常处理块中使用if、switch判断异常类型,但依然可以针对不同异常类型提供相应的处理逻辑,从而提供更细致,更有调理的异常处理逻辑。
从上图可以看出,通常情况下,如果try块被执行一次,则try块后只有一个catch块会被执行,绝不可能有多个catch块被执行,除非在循环中使用了continue开始下一次循环,下一次循环又重新运行了try块,这才可能导致多个catch块被执行。
注意:try块与if语句不一样,try块后的花括号({})不可以省略,即使try块里只有一行代码,也不可以省略这个花括号。与之类似的,catch块后的花括号({})也不可以省略。还有一点需要指出:try块里声明的变量是代码块内局部变量,它只在try块内有效,catch块中不能访问该变量。
Java提供了丰富的异常类,这些异常类之间有严格的继承关系,如下图:
从上图可以看出,Java把所有非正常情况分成两种:异常(Exception)和错误(Error),它们都是继承Throwable父类
Error错误,一般是指虚拟机相关问题,也就是JVM的问题,如系统崩溃、虚拟机出错误、动态链接失败等,这种错误是java程序的根本运行环境出现了问题,这样的错误Java程序无法恢复或不可能捕获,将导致应用程序中断,通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义该方法时,也无须在其throws子句声明该方法能抛出Error及其任何子类。
1 package chapter10; 2 3 public class TestDiv { 4 public static void main(String[] args){ 5 try{ 6 int a = Integer.parseInt(args[0]); 7 int b = Integer.parseInt(args[1]); 8 int c = a/b; 9 System.out.println("您输入的两个数的的商是:" + c); 10 }catch(IndexOutOfBoundsException i){ 11 System.out.println("参数个数不够"); 12 i.printStackTrace(); 13 }catch(NumberFormatException n){ 14 System.out.println("数字格式非法"); 15 n.printStackTrace(); 16 }catch(ArithmeticException a){ 17 System.out.println("算术异常"); 18 a.printStackTrace(); 19 }catch(Exception e){ 20 System.out.println("未知异常"); 21 e.printStackTrace(); 22 } 23 } 24 }
上面三种异常都是非常常见的异常,应该记住。
1 import java.util.*; 2 3 public class TestNull{ 4 public static void main(String[] args){ 5 Date d = null; 6 try{ 7 System.out.println(d.after(new Date())); 8 } 9 catch (NullPointerException ne){ 10 System.out.println("空指针异常"); 11 } 12 catch(Exception e){ 13 System.out.println("未知异常"); 14 } 15 } 16 }
上面程序针对NullPointerException异常提供了专门的异常处理块,上面程序调用一个null对象的after方法,这将引起NullPointerException(当试图调用一个null对象的实例方法和实例属性时,就会引发NullPointerException),Java运行时将会使用NullPointerException对应的catch块来处理该异常;如果程序遇到其他异常,Java运行时将会调用最后的catch块来处理该异常。
注意:我们总把对Exception类的catch块放在最后,原因是:如果我们把Exception对应的catch块排在其他catch块前面,Java运行时将直接进入该catch块(因为所有异常对象都是Exception或其子类的实例),而排在它后面的catch块将永远也不会获得执行的机会。
实际上,进行异常捕获时不仅应该把Exception对应的catch块放在最后,所有父类异常的catch块都应该排在子类异常catch块的后面(简称为:先处理子异常,再处理父异常),否则将出现编译错误。
2.3 访问异常信息
如果程序需要在catch块中访问异常对象的相关信息,可以通过调用catch后异常形参的方法来获得。当Java运行时决定调用某个catch块来处理该异常对象时,会将该异常对象赋给catch块后的异常参数,程序就可以通过该参数来获得该异常的相关信息。
所有异常对象都包含如下几个方法:
getMessage():返回该异常的详细描述字符串
printStackTrace():将该异常的跟踪栈信息输出到标准错误输出
printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
getStackTrace():返回该异常的跟踪栈信息。
如下例子演示了程序如何访问异常信息
1 package chapter10; 2 3 import java.io.*; 4 5 public class AccessExceptionMsg { 6 public static void main(String[] args){ 7 try{ 8 FileInputStream fis = new FileInputStream("a.txt"); 9 10 }catch(IOException i){ 11 System.out.println(i.getMessage()); 12 i.printStackTrace(); 13 } 14 } 15 }
上面程序调用了关于Exception类的两个方法来打印信息
运行上面的程序得到如下结果:
a.txt (系统找不到指定的文件。)java.io.FileNotFoundException: a.txt (系统找不到指定的文件。) at java.io.FileInputStream.open(Native Method) at java.io.FileInputStream.<init>(FileInputStream.java:120) at java.io.FileInputStream.<init>(FileInputStream.java:79) at chapter10.AccessExceptionMsg.main(AccessExceptionMsg.java:8)
2.4 使用finally回收资源
有些时候,程序try块里面打开了一些物理资源如数据库连接、网络连接和磁盘文件,这些资源必须显式回收。java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。那么在哪里回收这些物理资源呢?在try块里回收是不行的,如果在这里回收,那么如果在try块里的某条语句引起了异常,该语句后的其他语句通常得不到执行机会,那么该语句之后的资源回收语句得不到执行,如果在catch块里进行资源回收,但catch块完全有可能得不到执行,这将导致不能及时回收这些物理资源。
为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,finally块总会被执行。这样Java完整的异常处理机制如下:
1 try{ 2 //业务实现代码 3 ..... 4 }catch(SubException s1){ 5 //异常处理块 6 ..... 7 }catch(SubException s2){ 8 //异常处理块 9 ..... 10 }finally{ 11 //资源回收块 12 ...... 13 }
异常处理语法结构中只有try块是必须的,也就是说如果没有try块,则不能有catch块和finally块;catch块和finally块都是可选的,但catch块和finally块至少出现其中之一,也可以同时出现;可以有多个catch 块,捕获父类异常的catch块必须位于捕获子类异常的后面;但不能只有try块,既没有catch块,也没有finally块;多个catch块必须位于try块之后,finally块必须位于所有catch块之后。
1 package chapter10; 2 3 import java.io.*; 4 5 public class TestFinally { 6 public static void main(String[] args){ 7 FileInputStream fis = null; 8 try{ 9 fis = new FileInputStream("a.txt"); 10 }catch(IOException i){ 11 System.out.println(i.getMessage()); 12 //return语句强制方法返回 13 return; 14 //使用System.exit退出虚拟机 15 //System.exit(1); 16 }finally{ 17 //关闭磁盘文件,回收物理资源 18 if(fis !=null){ 19 try{ 20 fis.close(); 21 }catch(IOException i){ 22 i.printStackTrace(); 23 } 24 } 25 System.out.println("系统已经执行了垃圾回收!"); 26 } 27 28 } 29 } 30 输出结果: 31 a.txt (系统找不到指定的文件。) 32 系统已经执行了垃圾回收!
注意:当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等导致方法终止的语句,否则可能出现一些很奇怪的情况。
2.5 异常处理的嵌套
catch块中再次包含了一个完整的异常处理流程。这种在try块、catch块或finally块中包含完整的异常处理流程的情形被称为异常处理的嵌套。
异常处理流程代码可以放在任何能放可执行性代码的地方,因此完整的异常处理流程既可以放在try块里,也可以放在catch块里,也可以放在finally块里。
异常处理嵌套的深度没有很明确的限制,但通常没有必要使用超过两层的嵌套异常处理,层次太深的嵌套异常处理没有太大必要,而且导致程序可读性降低。
3 Checked异常和Runtime异常体系
Java异常被分为两大类:Checked异常和Runtime异常。所有RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。只有Java语言提供了Checked异常。
3.1 使用throws声明抛出异常
使用throws声明抛出异常的思路是:当前方法不知道应该如何处理这种类型的异常,该异常应该由上一级调用者处理,如果main方法也不知道应该如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常交给JVM处理。JVM对异常的处理方法是:打印异常跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因。
throws声明抛出只能在方法签名中使用,throws可以声明抛出多个异常类,多个异常类之间以逗号隔开。throws声明抛出的语法格式如下:
throws ExceptionClass1,ExceptionClass2....
上面throws声明抛出的语法格式仅跟在方法签名之后,如下例子程序使用了throws来声明抛出IOException,一旦使用throws语句声明抛出该异常,程序就无须使用try...catch块来捕获该异常。
1 package chapter10; 2 3 import java.io.*; 4 5 public class TestFinally { 6 public static void main(String[] args) throws IOException{ 7 FileInputStream fis = new FileInputStream("a.txt"); 8 } 9 }
上面程序声明程序不处理IOException,将该异常交给JVM处理,所以程序一旦遇到该异常,JVM就会打印该异常的跟踪信息,并结束程序。运行上面的程序,可以看到结果:
1 Exception in thread "main" java.io.FileNotFoundException: a.txt (系统找不到指定的文件。) 2 at java.io.FileInputStream.open(Native Method) 3 at java.io.FileInputStream.<init>(FileInputStream.java:120) 4 at java.io.FileInputStream.<init>(FileInputStream.java:79) 5 at chapter10.TestFinally.main(TestFinally.java:8)
如果某段代码中调用了一个带throws声明的方法,该方法声明抛出了Checked异常,这表明该方法希望它的调用者来处理该异常。也就是说,这段代码要么放在try块中显示捕获该异常,要么这段代码处于另一个带throws声明抛出的方法中
1 public class TestThrows2{ 2 public static void main(String[] args) throws Exception{ 3 //因为test()方法声明抛出IOException异常, 4 //所以调用该方法的代码要么处于try...catch块中, 5 //要么处于另一个带throws声明抛出的方法中。 6 test(); 7 } 8 9 public static void test()throws IOException{ 10 //因为FileInputStream的构造器声明抛出IOException异常, 11 //所以调用FileInputStream的代码要么处于try...catch块中, 12 //要么处于另一个带throws声明抛出的方法中。 13 FileInputStream fis = new FileInputStream("a.txt"); 14 } 15 }
使用throws声明抛出异常时有一个限制:子类方法中声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相等,子类方法中不允许比父类方法声明抛出更多异常。
1 import java.io.*; 2 3 public class OverrideThrows{ 4 public void test()throws IOException{ 5 FileInputStream fis = new FileInputStream("a.txt"); 6 } 7 } 8 class Sub extends OverrideThrows{ 9 //下面代码无法通过编译:子类方法声明抛出了比父类方法更大的异常 10 public void test()throws Exception{ 11 //... 12 } 13 }
上面程序中Sub子类的test方法声明抛出Exception,该Exception是其父类声明抛出异常IOException类的父类,这将导致程序无法通过编译。
由此可见使用Checked异常至少存在如下两大不便之处:
1.对于程序中Checked异常,Java要求必须显式捕获并处理该异常,或者显式声明抛出。
2.如果在方法中显式声明抛出Checked异常,将会导致方法签名与异常耦合,如果该方法是重写父类的方法,则该方法能抛出的异常还受到被重写方法所抛出异常的限制。
大部分情况下,推荐使用Runtime异常,而不是Checked异常。
3.2 使用throw抛出异常
当程序出现错误时,系统会自动抛出异常。除此之外java也允许自行抛出异常,自行抛出异常使用throw注意:不是throws
3.2.1 抛出异常
计算机中的异常是一种很主观的说法,它和数学中的错误是有明显区别的。数学中的错误往往是推理、数值计算这种属于普适性的,不会因为在哪里适用哪里不适用的问题。但计算机产品里,系统是否抛出异常,可能需要根据应用的业务需求来决定,如果程序中的数据、执行和既定的业务需求不符,那这就是一种异常。这种由于与业务需求不符而产生的异常,必须由程序员来决定抛出,系统无法抛出这种异常。
如果需要在程序中自行抛出异常,应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。throw语句的语法格式如下:
throw ExceptionInstance;
如果throw语句抛出的异常是Checked异常,则该throw语句要么处于try块,显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给该方法的调用者处理;如果throw语句抛出的异常是Runtime异常,则该语句无须放在try块里,也无须放在带throws声明抛出的方法中;程序既可以显式使用try...catch来捕获,并处理该异常,也可以完全不理会该异常,把该异常交给该方法调用者处理。
1 package chapter10; 2 3 public class TestThrow { 4 public static void throwChecked(int a) throws Exception{ 5 if(a > 0){ 6 //自行抛出Exception异常 7 //该代码必须处于try块里或处于带throws声明的方法中 8 throw new Exception("a的值大于0,不符合要求"); 9 } 10 } 11 public static void throwRuntime(int a){ 12 if(a > 0){ 13 //自行抛出RuntimeException异常,也可以显式捕获该异常 14 //也可以完全不理会该异常,把异常交给该方法的调用者处理 15 throw new RuntimeException("a的值大于0,不符合要求"); 16 } 17 } 18 public static void main(String[] args){ 19 try{ 20 //调用throws声明的方法,必须显式捕获该异常 21 //否则,必须在main方法中再次声明抛出 22 throwChecked(3); 23 }catch(Exception e){ 24 System.out.println(e.getMessage()); 25 } 26 //调用抛出Runtime异常的方法,既可以显式处理该异常,也可以不理会该异常 27 throwRuntime(-3); 28 } 29 }
3.3 自定义异常类
通常情况下,程序很少会自行抛出系统异常,因为异常的类名通常包含了该异常的有用信息。所以在选择抛出什么异常时,应该选择合适的异常类,从而可以明确地描述该异常情况。在这种情况下,应用程序常常需要抛出自定义异常。
用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时,通常需要提供两种构造器:一个是无参数的构造器;另一个是带字符串的构造器,这个字符串将作为该异常对象的详细说明(也就是异常对象的getMessage方法的返回值)
1 public class AuctionException extends Exception{ 2 //异常类无参数的构造器 3 public AuctionException(){} 4 //带一个字符串参数的构造器 5 public AuctionException(String msg){ 6 super(msg); 7 } 8 }
3.4 catch和throw同时使用
前面介绍的异常处理方式有两种
1.在出现异常的方法内捕获并处理异常,该方法的调用者将不能再次捕获该异常。
2.该方法签名中声明抛出该异常,将该异常完全交给方法调用者处理。
实际应用中往往需要更复杂的处理方式:当一个异常出血时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可以完全处理该异常。也就是说,异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常。这样就可以让该方法的调用者也能捕获到异常。
为了实现这种靠多个方法协作处理同一个异常的情形,可以通过在catch块中结合throw来完成。
1 public class TestAuction{ 2 private double initPrice = 30.0; 3 4 public void bid(String bidPrice) throws AuctionException{ 5 double d = 0.0; 6 try{ 7 d = Double.parseDouble(bidPrice); 8 } 9 catch (Exception e){ 10 //此处完成本方法中可以对异常执行的修复处理,此处仅仅是在控制台打印异常跟踪信息。 11 e.printStackTrace(); 12 //再次抛出自定义异常 13 throw new AuctionException("竞拍价必须是数值,不能包含其他字符!"); 14 } 15 if (initPrice > d){ 16 throw new AuctionException("竞拍价比起拍价低,不允许竞拍!"); 17 } 18 initPrice = d; 19 } 20 21 public static void main(String[] args){ 22 TestAuction ta = new TestAuction(); 23 try{ 24 ta.bid("df"); 25 } 26 catch (AuctionException ae){ 27 //main方法再次捕捉到bid方法中的异常。并对该异常进行处理 28 System.err.println(ae.getMessage()); 29 } 30 } 31 }
上面程序前一个try块的catch块,当该catch块捕捉到异常时,系统打印了该异常的跟踪信息,接着抛出一个AuctionException,通知该方法的调用者处理该AuctionException。所以程序中main方法,也就是bid方法调用者还可以再次捕获AuctionException异常,并将该异常的详细描述信息输出到标准错误。
这种catch和throw结合使用的情况在大型企业级应用非常常用。企业级应用对异常的处理通常需分成2个部分:一、应用后台需要通过日志来记录异常发生的详细情况;二、应用还需要根据异常向应用使用者传达某种提示。在这种情形下,所有异常都需要两个方法共同完成,也就必须将catch和throw结合使用。
3.5 异常链
对于真实的企业级应用而言,常常有严格的分层关系,层与层之间有非常清晰的划分,上层功能的实现严格依赖于下层的API,也不会夸层访问。如下图所示:
分层结构应用示意图:
对于一个遵守上图所示的应用,当业务逻辑层访问持久层出现SQLException异常时,程序不应该把底层的SQLException异常传到用户界面,原因如下:
1.对于正常用户而言,他们不想看到底层SQLException,SQLException对他们使用系统没有任何帮助
2.对于恶意用户而言,将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 }
这种把原始信息隐藏起来,仅向上提供必要的异常提示信息的处理方式,可以保证底层异常不会扩散到表现层,可以避免向上暴露太多的实现细节,这完全符合面向对象的封装原则。
这种把捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来的是一种典型的链式处理,也被称为异常链。
所有的Throwable子类在构造器中都可以接受一个cause对象作为参数。这个cause就用来表示原始异常,这样可以把原始异常传递给新的异常,使得即使你在当前位置创建并抛出了新的异常,你也能通过这个异常链追踪到异常最初发生的位置。如果我们希望上面SalException可以追踪到最原始的异常信息,则可以将该方法改写为
1 public calSal throws SalException{ 2 try{ 3 //实现结算工资的业务逻辑 4 .... 5 }catch(SQLException sqle){ 6 //把原始异常记录下来,留个管理员 7 ... 8 //下面异常中的message就是向用户的提示 9 throw new SalException(sqle); 10 }catch(Exception e){ 11 //把原始异常记录下来,留个管理员 12 .... 13 //下面异常中的message就是向用户的提示 14 throw new SalException(e); 15 } 16 }
这样定义SalException类如下
1 public class SalException extends Exception{ 2 public SalException(){} 3 public SalException(String msg){ 4 super(msg); 5 } 6 public SalException(Throwable t){ 7 super(t); 8 } 9 }
创建了这个SalException业务异常类后,就可以用它来封装原始异常,从而实现对异常的链式处理。
4 Java的异常跟踪栈
异常对象的printStackTrace方法用于打印异常的跟踪栈信息,根据printStackTrace方法的输出结果,这样可以找到异常的源头,并跟踪异常一路触发的过程。
1 class SelfException extends Exception{ 2 SelfException(){} 3 SelfException(String msg){ 4 super(msg); 5 } 6 } 7 public class TestPrintStackTrace{ 8 public static void main(String[] args)throws SelfException{ 9 firstMethod(); 10 } 11 public static void firstMethod()throws SelfException{ 12 secondMethod(); 13 } 14 public static void secondMethod()throws SelfException{ 15 thirdMethod(); 16 } 17 public static void thirdMethod()throws SelfException{ 18 throw new SelfException("自定义异常信息"); 19 } 20 } 21 22 运行后输出结果: 23 Exception in thread "main" chapter10.SelfException: 自定义异常信息 24 at chapter10.TestPrintStackTrace.thirdMethod(TestPrintStackTrace.java:20)——————异常源头 25 at chapter10.TestPrintStackTrace.secondMethod(TestPrintStackTrace.java:17)——————第二个 26 at chapter10.TestPrintStackTrace.firstMethod(TestPrintStackTrace.java:14)——————第三个 27 at chapter10.TestPrintStackTrace.main(TestPrintStackTrace.java:11)——————main方法
从上面的运行结果可以看出:异常从thirdMethod方法开始触发,传播到secondMethod方法,再传到firstMethod方法,最后传到main方法,在main方法终止,这个过程就是Java的异常跟踪栈。
在面向对象的编程中,大多数复杂操作都会被分解成一系列方法调用。这是因为:实现更好的可重用性,将每个可重用的代码单元定义成方法,将复杂任务逐渐分解为更易管理的小型子任务。由于一个大的业务功能需要多个对象来共同实现,在最终变成模型中,很多对象将通过一系列方法调用来实现通信,执行任务。
所以面向对象的应用程序运行时,经常会发生一系列方法调用,从而形成"方法调用栈"(栈是一种特殊的数据结构,后进先出,这与方法调用后调用,但是先打出异常的日志时序上有相似之处),异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获,或异常没有被处理后重新抛出了新异常),异常从发送异常的方法逐渐向外传播,首先传给该方法的调用者,该方法调用者再次传给其调用者.....直至最后传到main方法,如果main方法依然没有处理该异常,JVM会中止该程序,并打印异常的跟踪栈信息。
4.1 异常跟踪栈结构分析
第一行的信息详细显示了异常的类型和异常的详细消息。
接下来跟踪栈记录程序中所有的异常发生点,各行显示被调用方法中执行的停止位置,并标明类、类中的方法名、与故障点对应的文件的行。一行行地往下看,跟踪栈总是最内部的被调用方法逐渐上传,直到最外部业务操作的起点,通常就是程序的入口main方法或Thread类的run方法。
1 Exception in thread "main" chapter10.SelfException: 自定义异常信息//异常的类型和异常的详细信息 2 at chapter10.TestPrintStackTrace.thirdMethod(TestPrintStackTrace.java:20)//异常的方法调用顺序 3 at chapter10.TestPrintStackTrace.secondMethod(TestPrintStackTrace.java:17) 4 at chapter10.TestPrintStackTrace.firstMethod(TestPrintStackTrace.java:14) 5 at chapter10.TestPrintStackTrace.main(TestPrintStackTrace.java:11)
虽然printStackTrace()方法可以很方便地用于追踪异常的发生情况,可以用它来调试程序,但最后发布的程序中,应该避免使用它。而应该对捕获的异常进行适当的处理,而不是简单地将异常的跟踪栈信息打印出来。
5 异常处理规则
前面介绍了使用异常处理的优势、便捷之处,本节将进一步从程序性能优化、结构优化的角度给出异常处理的一般规则。成功的异常处理应该达到如下目标:
1.使程序代码混乱最小化。 2.捕捉并保留诊断信息。 3.通知合适的人员 4.采用合适的方式结束异常获得。
下面介绍达到这种效果的基本准则。
5.1 不要过度使用异常
Java异常机制确实很方便,但滥用异常机制也会带来一些负面影响。
过度使用异常主要由两个方面:
1.把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地抛出异常来代替所有错误处理。
2.使用异常处理来代替流程控制
熟悉了异常使用方法,程序设计者可能不再愿意编写繁琐的错误处理代码,而是简单的抛出异常。实际上这是不对的,对于完全已知的错误,应该编写处理这种错误的代码,增加程序的健壮性,对于普通的错误,应该编写处理这种错误的代码,增加程序的健壮性。只对外部的、不能确定和预知的运行时错误才使用异常。
必须指出:异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来代替正常的业务逻辑判断。
另外,异常机制的效率比正常流程控制效率差,所以不要使用异常处理来代替正常的程序流程控制。
1 //定义一个数组 2 String[] arr = {"Hello","Java","Spring"}; 3 //遍历数组 4 try{ 5 int i = 0; 6 while(true){ 7 System.out.println(arr[i++]); 8 } 9 }catch(ArrayIndexOutOfBoundsException e){ 10 11 }
上面的异常处理固然可以遍历数组,但这种处理方式可读性差,程序设计者"刻意"制造了原本可以避免的异常。采用下面的代码就会好很多。
for(int i = 0; i < arr.length; i++){ System.out.println(arr[i]); }
异常处理只用于程序无法处理的非正常情况,不要滥用异常处理来代替正常的控制流程。
5.2 不要使用过于庞大的try块
在一个try块里放置大量的代码看上去"很简单",但这种简单只是在编写程序时看上去简单,但因为try块里的代码过于庞大,业务过于复杂,就会造成try块中出现异常的可能性大大增加,从而导致分析异常原因的难度也大大增加。
而且当try块过于庞大时,就难免在try块后紧跟大量的catch块才可以针对不同异常提供不同的处理逻辑。同一个try块后紧跟大量catch块则需要分析它们之间的逻辑关系,反而导致了编程复杂度的增加。
5.3 避免使用Catch All语句
所谓catch all语句指的是一种异常捕获模块,它可以处理程序发送的所有可能异常。例如
1 try{ 2 //业务逻辑 3 }catch(Throwable t){ 4 //异常处理 5 }
不可否认,每个程序员都曾经使用过这种异常处理方式;但在编写关键程序的时候就应该避免使用这种方式的异常处理方式。这种处理方式有如下两点不足之处:
1.所有异常都采用相同的处理方式,这将导致无法对不同异常分情况处理,如果要分情况处理,则需要在catch块使用分支语句进行控制,这是得不偿失的做法。
2.这种捕获方式可能将程序中的错误、Runtime异常都应该导致程序终止的情况全部捕获到,从而压制了异常,如果出现了一些关键的异常,那个异常也会被静悄悄地忽略。
实际上,catch all语句不过是一种通过避免错误处理而加快编程进度的机制,应尽量避免在实际应用中使用这种语句。
5.4 不要忽略捕获到的异常
不要忽略异常!既然捕获异常,那catch块理应做些有用的事情————处理并修复这个错误,catch块整个为空或者仅仅打印出错信息都是不妥的。catch块为空白就是假装不知道甚至瞒天过海,这是最可怕的事情————程序出了错误,但是所有人都看不到异常,但是应用可能意见彻底坏了。仅在catch块里打印错误跟踪栈信息稍微好一点,但仅仅比空白多了几行异常信息。通常建议对异常进行适当措施,比如:
1.处理异常,对异常采用合适的修补,然后绕过异常发生的地方继续执行;或者用别的数据进行计算机,以代替期望的方法返回值,或者提示用户重新操作.....总之对于checked异常,程序应该尽量采用修复
2.重新抛出新异常,把当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新抛出给上层调用者
3.在合适的层处理异常。如果当前层不清楚如何处理异常,就不要在当前层使用catch语句来捕获该异常,直接使用throws声明抛出该异常,让上层调用者来负责处理该异常。
6 本章总结
本章主要介绍了Java异常处理机制的相关知识,Java的异常处理主要依赖于try、catch、finally、throw和throws 五个关键字,本章详细讲解了这5个关键字,本章还介绍了Java异常类之间的继承关系,并介绍了CheckedException和Runtime异常之间的区别。本章还详细讲解了实际开发中最常用的异常链和异常转译。本章最后还从优化程序的角度,给出了实际应用中处理异常的几条基本规则。