第 5 章Java语言异常处理 面向对象程序设计特别强调软件质量的两个方面:一是程序结构方面 的可扩展性与可重用性;二是程序语法与语义方面的可靠性。可靠性 (reliability)是软件质量的关键因素,一个程序的可靠性体现在两方面:一 是程序的正确性(corectnes),指程序的实现是否满足了需求;二是程序的 健壮性(robustnes),指程序在异常条件下的执行能力。 在Java程序中,由于程序员的疏忽和环境因素的变化,经常会出现异 常情况,导致程序非正常终止。为了及时有效地处理程序运行中的错误, Java语言在参考C++语言的异常处理方法和思想的基础上,提供了一套 优秀的异常处理(exceptionhandling)机制,就可以有效地预防错误的程序 代码或系统错误所造成的不可预期的结果发生。异常处理机制通过对程 序中所有的异常进行捕获和恰当的处理来尝试恢复异常发生前的状态,或 对这些错误结果做一些善后处理。异常处理机制能够减少编程人员的工 作量,增加程序的灵活性,有效地增强程序的可读性和可靠性。 5.概 1 述 在程序运行时打断正常程序流程的任何不正常的情况称为错误或异 常。可能导致异常发生的原因有许多,例如下列的情形: ● 试图打开的文件不存在; ● 网络连接中断; ● 空指针异常;例如对一个值为nul 的引用变量进行操作; 算术异常;例如除数为0,操作符越界等; ● ● 要加载的类不存在。 下面是一个简单的程序,程序中声明了一个字符串数组,并通过一个 for循环将该数组输出。如果不认真地阅读分析程序,一般不容易发现程 序中可能导致异常的代码。 1 46 Java 程序设计———基于JDK 9 和NetBeans 实现 【例5.1】 Java程序异常示例。 1 public class Test { 2 public static void main(String[]args) { 3 String friends[]={"Lisa", "Mary", "Bily"}; 4 for(int i=0;i<4;i++){ 5 System.out.println(friends[i]); 6 } 7 System.out.println("Normal ended."); 8 } 9 } 【运行结果】 Lisa Mary Bily Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3 at Test.main (Test.java: 5) 【分析讨论】 ● 程序Test.java能够通过编译,但运行时出现异常,导致程序的非正常终止。 ● 程序在执行for循环语句块时,前3次依次输出String类型的数组friends包含的 3个元素,即执行结果的前3行。但是在第4次循环时,由于试图输出下标为3的 数组元素,而数组的长度为3,从而导致数组下标越界。产生异常的是第5句,异 常类型是java.lang.ArrayIndexOutOfBoundsException,并且系统自动显示了有关 异常的信息,指明异常的种类和出错位置。 在Java程序中,由于程序员的疏忽和环境因素的变化,会经常出现异常情况。如果 不对异常进行处理,将导致程序非正常终止。为保证程序正常运行,Java语言专门提供 了异常处理机制。Java语言首先针对各种常见的异常定义了相应的异常类,并建立了异 常类体系,如图5.1所示。 其中,java.lang.Throwable类是所有异常类的父类。Java语言中只有Throwable类 及其子类的对象才能由异常处理机制进行处理。该类提供的主要方法包括检索异常相关 信息,以及输出显示异常发生位置的堆栈追踪轨迹。Java语言异常类体系中定义了很多 常见的异常,例如: ● ArithmeticException:算术异常,整数的除0操作将导致该异常发生,例如,inti= 10/0。 ● NullPointerException:空指针异常,当对象没有实例化时,就试图通过该对象的 变量访问其数据或方法。 ● IOException:输入/输出异常,即进行输入/输出操作时可能产生的各种异常。 ● SecurityException:安全异常,一般由浏览器抛出。例如,Applet在试图访问本地 文件、试图连接该Applet所来自主机之外的其他主机,或试图执行其程序时,浏览 器中负责安全控制的SecurityManager类都要抛出这个异常。 第 5 章 Java 语言异常处理 71 图5.aa异常类继承层次 1 Jv Java语言中的异常可分为如下两类。 ● 错误(Eror)及其子类:JVM系统内部错误、资源耗尽等很难恢复的严重错误、不 能简单地恢复执行,一般不由程序处理。 ● 异常(Exception)及其子类:其他因编程错误或偶然的外在因素导致的一般性问 题,通过某种修正后程序还能继续运行,其中还可以分为 ■RuntimeException(运行时异常):也称为不检查异常,是指因为设计或实现方 式不当导致的问题,例如,数组使用越界、算术运算异常、空指针异常等。也可以 说是程序员的原因导致的、本来可以避免发生的情况。正确设计与实现的程序 不应产生这些异常。对于这类异常,处理的策略是纠正错误。 ■ 其他Exception类:描述运行时遇到的困难,它通常由环境而非程序员的原因引 起,如文件不存在、无效URL等。这类异常通常是由用户的误操作引起的,可 以在异常处理中进行处理。 例5.1中的异常就属于RuntimeException。出错的原因是数组friends中只含有3 个元素,当for循环执行到第4次时,试图访问根本不存在的第4个数组元素friends[3], 因此出错。 5.异常处理机制 2 异常处理是指程序获得异常并处理,然后继续程序的执行。Java语言要求如果程序 中调用的方法有可能产生某种类型的异常,那么调用该方法的程序必须采取相应的动作 1 48 Java 程序设计———基于JDK 9 和NetBeans 实现 处理异常。异常处理机制具体有两种方式:一是捕获并处理异常;二是将方法中产生的 异常抛出。 5.2.1 捕获并处理异常 Java程序在执行过程中如果出现异常,将会自动生成一个异常对象,该对象包含了 有关异常的信息,并被自动提交给Java运行时系统,这个过程称为抛出异常。当Java运 行时系统接收到异常对象时,会寻找能处理这一异常的代码并把当前异常对象交给其处 理,这一过程称为捕获异常。如果Java运行时系统找不到可以捕获异常的方法,则运行 时系统将终止,相应的Java程序也将退出。 try-catch-finally语句用于捕获程序中产生的异常,然后针对不同的异常采用不同的 处理程序进行处理。try-catch-finally语句的基本语法如下: try { Java statements //一条或多条可能抛出异常的Java 语句 } catch(ExceptionType1 e) { Java statements //当ExceptionType1 类型的异常抛出后要执行的代码 } catch(ExceptionType2 e) { Java statements //当ExceptionType2 类型的异常抛出后要执行的代码 } [finally { Java statements //执行最终清理的语句,即无条件执行的语句 } ] try-catch-finally语句把可能产生异常的语句放入try{}语句块中,然后在该语句块 后跟一个或多个catch语句块,每个catch块处理一种可能抛出的特定类型的异常。在运 行时刻,如果try{}语句块产生的异常与某个catch语句处理的异常类型相匹配,则执行 该catch语句块。finally语句定义了一个程序块,可放于try{}和catch{}块之后,用于 为异常处理提供一个统一的出口,使得在控制流转到程序的其他部分以前,能够对程序的 状态进行统一的管理。不论在try{}语句块中是否发生了异常事件,finally块中的语句 都会被执行。finally语句是可选的,可以省略。 需要注意的是,用catch语句进行异常处理时,可以使一个catch块捕获一种特定类 型的异常,也可以定义处理多种类型异常的通用catch块。因为在Java语言中允许对象 变量上溯造型,父类类型的变量可以指向子类对象,所以如果catch语句块要捕获的异常 类还存在其子类,则该异常处理块就将可以处理该异常类以及其所有子类表示的异常事 件。这样一个catch语句块就是一个能够处理多种异常的通用异常处理块。例如,在下 列语句中,catch语句块将处理Exception类及其所有子类类型的异常,即处理程序能够 处理的所有类型的异常。 try { ... }catch(Exception e) { System.out.println("Exception caugth: "+e.getMessage()); } 第5 章 Java 语言异常处理 1 49 接下来看一看上述机制是如何处理例5.1中的问题的。 【例5.2】 采用try-catch-finally对例5.1中的RuntimeException进行异常处理。 1 public class Test2 { 2 public static void main(String[]args) { 3 String friends[]={"Lisa", "Mary", "Bily"}; 4 try{ 5 for(int i=0;i<4;i++) { 6 System.out.println(friends[i]); 7 } 8 }catch(ArrayIndexOutOfBoundsException e) { 9 System.out.println("Index error"); 10 } 11 System.out.println("\nNormal ended."); 12 } 13 } 【运行结果】 Lisa Mary Bily Index error Normal ended 【分析讨论】 ● 从输出结果可以看出,出现异常时参数类型匹配的catch语句块得到执行,程序输 出提示性信息后继续执行,并未异常终止,此时程序的运行仍处于程序员的控制之 下。那么,既然运行错误经常发生,是不是所有的Java程序也都采取这种异常处 理措施呢? 答案是否定的,Java程序异常处理的原则是: ① 对于Error和RuntimeException,可以在程序中进行捕获和处理,但不是必 须的。 ② 对于IOException及其他异常类,必须在程序中进行捕获和处理。 ● 例5.1和例5.2中的ArrayIndexOutOfBoundsException就属于RuntimeException, 一个正确设计和实现的程序不会出现这种异常,因此可以根据实际情况选择是否 需要进行捕获和处理。而IOException及其他异常则属于另外一种必须进行捕获 和处理的情况。 再看一个IOException 的例子。例5.3 的类CreatingList要创建一个保存5 个 Integer对象的数组链表,并通过copyList()方法将该链表保存到FileList.txt文件中。 【例5.3】 创建链表并将其保存到文件中(未加任何异常处理,存在编译错误)。 1 import java.io.*; 2 import java.util.*; 1 50 Java 程序设计———基于JDK 9 和NetBeans 实现 3 class CreatingList { 4 private ArrayList list; 5 private static final int size=5; 6 public CreatingList () { 7 list=new ArrayList(size); 8 for(int i=0;i<size;i++) 9 list.add(new Integer(i)); 10 } 11 //将list 保存到FileList.txt 文件中 12 public void copyList() { 13 BufferedWriter bw=new BufferedWriter(new FileWriter("FileList.txt")); 14 for(int i=0;i<size;i++) { 15 bw.write("Value at: "+i+"="+list.get(i)); 16 bw.newLine(); 17 } 18 bw.close(); 19 } 20 } 21 public class ListDemo1 { 22 public static void main(String[]args) { 23 CreatingList clist=new CreatingList (); 24 clist.copyList(); 25 } 26 } 【编译结果】 ListDemo1.java: 13: 未报告的异常java.io.IOException; 必须对其进行捕捉或声明以便 抛出 BufferedWriter bw=new BufferedWriter(new FileWriter("FileList.txt")); ^1 错误 【分析讨论】 例5.3的第13行语句调用java.io.FileWriter的构造方法创建了一个文件输出流。 该构造方法的声明如下:publicFileWriter(StringfileName)throwsIOException。由 于copyList()方法中没有对FileWriter构造方法可能产生的异常进行处理,所以程序在 编译时产生了上述错误。 例5.4是在例5.3中加入了异常处理机制。将例5.3中的第13~18行语句放入try 语句块中,用两个catch语句分别捕获FileWriter("FileList.txt")调用中可能产生的 IOException 异常,以及for 循环访问链表的list.get(i)方法时可能产生的 ArrayIndexOutOfBoundsException异常。try-catch语句和finally语句最后执行程序的 清理操作,此处是关闭程序打开的流。 【例5.4】 增加try-catch-finally异常处理后的例5.3。 第5 章 Java 语言异常处理 1 51 1 import java.io.*; 2 import java.util.*; 3 class CreatingList { 4 private ArrayList list; 5 private static final int size=5; 6 public CreatingList () { 7 list=new ArrayList(size); 8 for(int i=0;i<size;i++) 9 list.add(new Integer(i)); 10 } 11 public void copyList() { 12 BufferedWriter bw=null; 13 try { 14 System.out.println("Catching Exceptions"); 15 bw=new BufferedWriter(new FileWriter("FileList.txt")); 16 for(int i=0;i<size;i++) { 17 bw.write("Value at: "+i+"="+list.get(i)); 18 bw.newLine(); 19 } 20 bw.close(); 21 }catch(ArrayIndexOutOfBoundsException e) { //处理数组越界异常 System.out.println("Caught ArrayIndexOutOfBoundsException. "); 22 }catch(IOException e){ //处理I/O 异常 23 System.out.println("Caught IOException."); 24 } 25 System.out.println("Closing BufferedWriter, Normal Ended! "); 26 } 27 } 28 public class ListDemo2 { 29 public static void main(String[]args) { 30 CreatingList clist=new CreatingList (); 31 clist.copyList(); 32 } 33 } 【运行结果】 Catching Exceptions Closing BufferedWriter, Normal Ended! 5.2.2 将方法中产生的异常抛出 将方法中产生的异常抛出是Java语言处理异常的第二种方式。如果一个方法中的 语句执行时可能生成某种异常,但是并不能或不确定如何处理这种异常,则该方法应声明 抛出该种异常,表明该方法将不对此类异常进行处理,而由该方法的调用者负责处理。 1 52 Java 程序设计———基于JDK 9 和NetBeans 实现 1.使用throws关键字抛出异常 将异常抛出,可通过throws关键字实现。throws关键字通常被应用在声明方法时, 用来指定方法可能抛出的异常,其语法格式如下: <modifer> <returnType> methodName ([<argument list>]) throws <exceptionList> 其中,exceptionList可以包含多个异常类型,用逗号隔开。 例5.4是使用try-catch-finally语句实现例5.3的异常处理的。例5.5是采用异常处 理的第二种方式对例5.3进行改进。 【例5.5】 采用声明抛出异常的方法对例5.3进行异常处理。 1 import java.io.*; 2 import java.util.*; 3 class CreatingList{ 4 private ArrayList list; 5 private static final int size=5; 6 public CreatingList(){ 7 list=new ArrayList(size); 8 for(int i=0;i<size;i++) 9 list.add(new Integer(i)); 10 } //声明抛出异常 11 public void copyList() throws IOException, ArrayIndexOutOfBoundsException{ 12 BufferedWriter bw=new BufferedWriter(new FileWriter("FileList.txt")); 13 for(int i=0;i<size;i++){ 14 bw.write("Value at: "+i+"="+list.get(i)); 15 bw.newLine(); 16 } 17 bw.close(); 18 } 19 } 20 public class ListDemo3{ 21 public static void main(String[]args){ 22 try{ 23 CreatingList clist=new CreatingList(); 24 clist.copyList(); 25 }catch(ArrayIndexOutOfBoundsException e){ //处理数组越界异常 System.out.println("Caught ArrayIndexOutOfBoundsException."); 26 }catch(IOException e){ //处理I/O 异常 27 System.out.println("Caught IOException."); 28 } 29 System.out.println("A list of numbers is created and stored in FileList. txt"); 30 } 第5 章 Java 语言异常处理 1 53 31 } 【运行结果】 A list of numbers is created and stored in FileLIst.txt 【分析讨论】 如果被抛出的异常在调用程序中未被处理,则该异常将被沿着方法的调用关系继续 上抛,直到被处理。如果一个异常返回到main()方法,并且在main()方法中还未被处 理,则该异常将把程序非正常地终止。 2.使用throw 关键字 使用throw关键字也可抛出异常,与throws不同的是,throw 用于方法体内,并且抛 出一个异常类对象,而throws用在方法声明中来指明方法可能抛出的多个异常。 通过throw抛出异常后,如果想由上一级代码捕获并处理异常,同样需要在抛出异常 的方法中使用throws关键字在方法的声明中指明要抛出的异常;如果想在当前的方法中 捕获并处理throw抛出的异常,则必须使用try-catch-finally语句。throw 语句的一般格 式如下: throw someThrowableObject; 其中someThrowableObject必须是Throwable类或其子类的对象。执行throw 语 句后,运行流程立即停止,throw的下一条语句将暂停执行,系统转向调用者程序,检查是 否有与catch子句匹配的Throwable实例对象。如果找到相匹配的实例对象,则系统转 向该子句;如果没有找到,则转向上一层的调用程序。这样逐层向上,直到最外层的异常 处理程序终止程序并打印出调用栈的情况。 例如,当输入的年龄为负数时,Java虚拟机当然不会认为这是一个错误,但实际上年 龄是不能为负数的,可以通过异常的方式处理这种情况。例5.6中创建了People类,该 类中的check()方法首先将传递进来的String型参数转换为int型,然后判断该int型整 数是否为负数,若为负数,则抛出异常;然后在该类的main()方法中捕获异常并处理。 【例5.6】 throw关键字的使用示例。 1 public class People { 2 public static int check(String strage) throws Exception { 3 int age=Integer.parseInt(strage); //转化字符串为int 型 4 if(age<0) //如果age 小于0,则抛出一个Exception 异常对象 5 throw new Exception("年龄不能为负数!"); 6 return age; 7 } 8 public static void main(String args[]) { 9 try { 10 int myage=check("-101"); //调用check()方法 11 System.out.println(myage); 1 54 Java 程序设计———基于JDK 9 和NetBeans 实现 12 }catch(Exception e) { / /捕 获Exception 异常 13 System.out.println("数据逻辑错误!"); 14 System.out.println("原因: "+e.getMessage()); 15 } 16 } 17 } 【运行结果】 数据逻辑错误! 原因: 年龄不能为负数! 【分析讨论】 在check()方法中将异常抛给了调用者(main()方法)进行处理。check()方法可能 抛出两种异常: ① 数字格式的字符串转换为int型时抛出的NumberFormatException异常; ② 当年龄小于0时抛出的Exception异常。 5.3 自定义异常类 通常,使用Java内置的异常类就可以描述在编写程序时出现的大部分异常情况。但 有时仍然需要根据需求创建自己的异常类,并将它们用于程序中来描述Java内置异常类 所不能描述的一些特殊情况。下面介绍如何创建和使用自定义异常类。 5.3.1 必要性与原则 Java语言允许用户在需要时创建自己的异常类型,用于表述JDK中未涉及的其他异 常状况,这些类型也必须继承Throwable类或其子类。Throwable类有两种类型的子类, 即错误(Error)和异常(Exception)。大多数Java应用都抛出异常,错误是指系统内部发 生的严重错误。所以,一般自定义异常类都以Exception类为父类。由于用户自定义异 常类通常属于Exception范畴,因此依据命名惯例,自定义异常类的名称应以Exception 结尾。由于定义异常类未被加入JRE的控制逻辑中,因此永远不会自动抛出,只能由人 工创建并抛出。 例如,假设要编写一个可重用的链表类,该类中可能包括如下方法: ● objectAt(intn):返回链表中的第n个对象。 ● firstObject():返回链表中的第一个对象。 ● indexOf(Objectn):在链表中搜索指定的对象,并返回它在链表中的位置。 对于这个链表类,在其他程序员使用时可能出现对类及方法使用不当的情况,并且即 使是合法的方法调用,也有可能导致某种未定义的结果。因此,希望这个链表类在出现错 误时尽量强壮,能够合理地处理错误,并把错误信息报告给调用程序。但是,由于不能预 知使用该类的每个用户如何处理特定的错误,所以在发生一种错误时最好的处理办法是 抛出一个异常。