异常处理与输入输出 流 安全是Java语言考虑的重要因素之一。Java通过异常处理机制对程序执行过程中与 预期不符的各种意外情况进行捕获与处理。异常(Exception), 又称为例外,是特殊的运行 错误对象。当程序运行过程中遇到异常时,异常处理机制会将程序流程导向到专门的错误 处理模块。 一个程序需要从外部获取(输入)信息,这个“外部”范围很广,包括诸如键盘、显示器、文 件、磁盘、网络、另外一个程序等;“信息”也可以是任何类型的,例如一个对象、一串字符、图 像、声音等。在Java程序设计中,可以通过使用jvio包(又称IO 包)中的输入输出流类 aa. 达到输入输出信息的目的。 这一章首先介绍Java的异常处理机制及方法,然后介绍输入输出流类的基本概念和常 用的文件读写方式。通过这一章的学习并参考JDK 文档,读者可以学习到如何在程序中处 理各种异常情况,并可以编写涉及不同信息源和信息类型的输入输出流程序。 5.异常处理机制简介 1 1.异常处理概述 5.1 异常是在程序运行过程中发生的异常事件,例如除0溢出、数组越界、文件找不到等,这 些事件的发生将阻止程序的正常运行。为了提高程序的鲁棒性,在进行程序设计时,必须考 虑到可能发生的异常事件并做出相应的处理。 Java中声明了很多异常类,每个异常类都代表了一种运行错误,类中包含了该异常的 信息和处理异常的方法等内容。每当Java程序运行过程中发生一个可识别的运行错误(即 有一个异常类与该错误相对应时), 系统就会产生一个相应的该异常类的对象,然后采取相 应的机制来处理它,确保不会对操作系统产生损害,从而保证了整个程序运行的安全性。这 就是Java的异常处理机制。 Java通过面向对象的方法来处理程序错误,为可能产生非致命性错误的代码段设计错 误处理模块,将错误作为预定义好的“异常”捕获,然后传递给专门的错误处理模块进行处 理。在一个方法的运行过程中,如果发生了异常,那么Java虚拟机便会生成一个代表该异 常的对象,并把它交给运行时系统,运行时系统便寻找相应的代码来处理这一异常。我们把 生成异常对象并提交给运行系统的过程称为抛出(throw)一个异常。 运行时系统在方法的调用栈中查找,从生成异常的方法开始进行回溯,直到找到包含处 理相应异常的方法为止,这一个过程称为捕获(catch)一个异常。 第5章异常处理与输入输出流·153· 使用Java的异常处理机制进行错误处理有以下优点: .将错误处理代码从常规代码中分离出来。 .按错误类型和差别分组。 .能够捕获和处理那些难以预测的错误。 .克服了传统方法中错误信息有限的问题。 .把错误传播给调用堆栈。 图5-1是对异常处理的示意图。可以看到,图中所示程序的调用过程是从方法1~4依 次调用,方法4探测并抛出异常,由方法1捕获并处理。 在程序实际执行过程中,如果在方法4中发生了异常, 则程序将从该异常点沿调用栈进行回溯,直到找到对异 常进行处理的方法为止,即图中的方法1。同样,如果在 方法2中对异常进行了捕获,那么回溯就不会到达方法 1,而是在方法2就完成了。图5- 1 异常处理示意图 1.错误的分类 5.2 广义来说,程序中的错误可以分为三类,即编译错误、运行错误和逻辑错误。编译错误 是编译器能够检测到的错误,一般为语法错误;运行错误是运行时产生的错误,如被零除、数 组下标越界等;而逻辑错误是机器本身无法检测的,需要人对运行结果及程序逻辑进行认真 分析。逻辑错误有时会导致运行错误。 Java系统中根据错误的严重程度不同,而将错误分为两类: .错误(Eror):是致命性的,即程序遇到了非常严重的不正常状态,不能简单地恢复 执行。 .异常(Exception):是非致命性的,通过某种修正后程序还能继续执行。 Exception类是所有异常类的超类;Eror类是所有错误类的超类。这两个类同时又是 Throwable的子类。 异常和错误类的层次结构如图5-2所示。 图5- 2 异常和错误类的层次结构 从图5-2 可以看到,Error类下的错误都是严重的错误,用户程序无法进行处理; Exception下的异常又分为两类:检查型异常和非检查型异常。 一些因为编程错误而导致的异常,或者是不能期望程序捕获的异常(例如数组越界、除 零,等等),称为非检查型异常(继承自RuntimeException)。非检查型异常不需要在方法中 声明抛出,且编译器对这类异常不做检查。 其他类型的异常称为检查型异常,Java类必须在方法签名中声明它们所抛出的任何 检查型异常。对于任何方法,如果它调用的方法抛出一个类型为E的检查型异常,那么 调用者就必须捕获E或者也声明抛出E(或者抛出E的一个超类),对此编译器要进行 检查。 例5-1 测试系统定义的运行异常———数组越界出现的异常。 程序如下: public class HelloWorld { public static void main(String args[]) { int i = 0; String greetings[] = {"Hello world!", "No, I mean it!", "HELLO WORLD!!"}; while (i < 4) { System.out.println(greetings[i]); i++; } } } 输出结果如下: Hello world! No, I mean it! HELLO WORLD!! Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException at HelloWorld.main(HelloWorld.java:7) 上面是一个简单的Java程序,访问数组元素时,运行时环境会根据数组的长度检查 下标是否越界。由于声明数组的长度为3,当访问数组下标为3的元素时出现越界,就导 致了ArrayIndexOutOfBoundsException异常。这个异常是系统定义好的异常类,对应系 统可识别的错误,所以Java虚拟机就会自动中止程序的执行流程,并新建一个 ArrayIndexOutOfBoundsException类的对象,然后抛出这个异常。 Java预定义了一些常见异常,例如: 1.ArithmeticException 整数除法中,如果除数为0,则发生该类异常。 如: int i = 12 / 0; ·154· Java语言程序设计(第3版) 2.NullPointerException 如果一个对象还没有实例化,那么访问该对象或调用它的方法将导致 NullPointerException异常。 如: Image im [] = new Image [4]; System.out.println(im[0].toString()) 3.NegativeArraySizeException 数组的元素个数应是一个大于等于0的整数。创建数组时,如果元素个数是个负数,则 会引发NegativeArraySizeException异常。 4.ArrayIndexOutOfBoundsException 把数组看作是对象,并用length变量记录数组的大小。访问数组元素时,运行时环境会根据 数组的长度检查下标是否越界。如果数组下标越界,则将导致ArrayIndexOutOfBoundsException 异常。 5.ArrayStoreException 程序试图存取数组中错误的数据类型。 6.FileNotFoundException 试图访问一个不存在的文件。 7.IOException 通常的I/O 错误。 5.2 异常的处理 对于检查型异常,Java程序必须进行处理。处理方法有如下两种: . 声明抛出异常:不在当前方法内处理异常,而是把异常抛出到调用当前方法的方 法中。 . 捕获异常:在当前方法中使用catch语句块捕获所发生的异常,并进行相应的处理。 5.2.1 声明抛出异常 如果程序员不想在当前方法内处理异常,可以使用throws子句声明将异常抛出到调用 当前方法的方法中。如: public void openThisFile(String fileName) throws java.io.FileNotFoundException { //code for method } 一个throws子句也可以声明抛出多个异常,如: public Object convertFileToObject(String fileName) throws java.io.FileNotFoundException, java.lang.ClassNotFoundException { 第5章 异常处理与输入输出流·155· //code for method } 调用方法也可以将异常再抛给它的调用方法,如: public void getCustomerInfo() throws java.io.FileNotFoundException { //do something this.openThisFile("customer.txt"); //do something } 上例中,如果在openThisFile方法中抛出了异常,getCustomerInfo方法将停止执行, 并将此异常传送给调用getCustomerInfo方法的方法中。 如果所有的方法都选择了抛出此异常,都没有捕获处理,最后Java虚拟机将捕获它们, 输出相关的错误信息,并终止程序的运行。在异常被抛出之后,调用栈中的任何方法都可 以捕获异常并进行相应的处理。 5.2.2 捕获异常 Java程序设计中可以使用try语句括住可能抛出异常的代码段,然后紧接着用catch语 句指明要捕获的异常及相应的处理代码。 try与catch语句的语法格式如下: try { //此处为抛出具体异常的代码 } catch (ExceptionType1 e) { //抛出ExceptionType1 异常时要执行的代码 }catch (ExceptionType2 e) { //抛出ExceptionType2 异常时要执行的代码 ... }catch (ExceptionTypek e) { //抛出ExceptionTypek 异常时要执行的代码 }finally { //必须执行的代码 } 其中,ExceptionType1、ExceptionType2…ExceptionTypek是产生的异常类型。在运 行时,根据产生的异常类型找到对应的catch语句,然后执行catch语句块中的代码部分。 finally语句块的作用通常用于释放资源,它不是必须的部分,如果有finally语句块,不论是 否捕获到异常,总要执行finally语句块中的代码。 在有多个异常需要捕获时,异常类型的顺序很重要,具体的异常类型要放在一般异常类 型的前面。例如,下面程序中的顺序就不合理,如果发生FileNotFoundException异常,由 于第一个catch语句块中声明的Exception是FileNotFoundException的超类,因此异常总 是会被第一个catch语句块捕获,永远无法到达第二个catch语句块。 ·156· Java语言程序设计(第3版) public void getCustomerInfo() { try { //do something that may cause an Exception } catch (java.lang.Exception ex) { //Catches all Exceptions } catch (java.io.FileNotFoundException ex) { //Never reached since above two are caught first } } 在catch块的内部,可用下面的方法处理异常对象: . getMessage()———返回一个对产生的异常进行描述的字符串。 . printStackTrace()———输出运行时系统的方法调用序列。 例5-2 捕获异常举例。 实现功能:读入两个整数,第一个数除以第二个数,之后输出。 public class ExceptionTester1 { public static void main(String args[]) { System.out.println("Enter the first number:"); int number1 = Keyboard.getInteger(); System.out.println("Enter the second number:"); int number2 = Keyboard.getInteger(); System.out.print(number1 + " / " + number2 + "="); int result = number1 / number2; System.out.println(result); } } 其中,Keyboard类的声明如下: import java.io.*; public class Keyboard { static BufferedReader inputStream = new BufferedReader(new InputStreamReader(System.in)); public static int getInteger() { try { return (Integer.valueOf(inputStream.readLine().trim()).intValue()); } catch (Exception e) { e.printStackTrace(); return 0; } } public static String getString() { try { 第5章 异常处理与输入输出流·157· return (inputStream.readLine()); } catch (IOException e) { return "0"; } } } 如果依次输入“143”和“24”,则输出结果如下: Enter the first number: 143 Enter the second number: 24 143 / 24=5 如果依次输入“140”和“abc”,则系统会报告异常,结果如下: Enter the first number: 140 Enter the second number: abc java.lang.NumberFormatException: For input string: "abc" at java.lang.NumberFormatException.forInputString (NumberFormatException.java:48) at java.lang.Integer.parseInt(Integer.java:449) at java.lang.Integer.valueOf(Integer.java:554) at Keyboard.getInteger(Keyboard.java:6) at ExceptionTester.main(ExceptionTester.java:7) Exception in thread "main" java.lang.ArithmeticException: / by zero at ExceptionTester.main(ExceptionTester.java:9) 140 / 0=Java Result: 1 在Keyboard.getInteger方法中,捕获任何Exception类的异常,并输出相关信息。 为了处理输入的字母(而非数字),可以使用方法Keyboard.getString先取得字符串后, 然后再做转换。 例5-3 捕获NumberFormatException类型的异常。 public class ExceptionTester2 { public static void main(String args[]) { int number1 = 0, number2 = 0; try { System.out.println("Enter the first number:"); number1 = Integer.valueOf(Keyboard.getString()).intValue(); System.out.println("Enter the second number:"); number2 = Integer.valueOf(Keyboard.getString()).intValue(); ·158· Java语言程序设计(第3版) } catch (NumberFormatException e) { System.out.println("Those were not proper integers!quit!"); System.exit(-1); } System.out.print(number1 + " / " + number2 + "="); int result = number1 / number2; System.out.println(result); } } 运行结果如下: Enter the first number: abc Those were not proper integers! I quit! 例5-4 捕获被零除的异常(ArithmeticException类型的异常)。 public class ExceptionTester3 { public static void main(String args[]) { int number1 = 0, number2 = 0, result = 0; try { System.out.println("Enter the first number:"); number1 = Integer.valueOf(Keyboard.getString()).intValue(); System.out.println("Enter the second number:"); number2 = Integer.valueOf(Keyboard.getString()).intValue(); result = number1 / number2; } catch (NumberFormatException e) { System.out.println("Invalid integer entered!"); System.exit(-1); } catch (ArithmeticException e) { System.out.println("Second number is 0, cannot do division!"); System.exit(-1); } System.out.print(number1 + " / " + number2 + "=" + result); } } 输出结果如下: Enter the first number: 143 Enter the second number: 0Second number is 0, cannot do division! 第5章 异常处理与输入输出流·159· 下面对程序进行改进:重复提示输入,直到输入合法的数据。为了避免代码重复,可将 数据存入数组。 例5-5 可以提示重复输入的程序。 public class ExceptionTester4 { public static void main(String args[]) { int result; int number[] = new int[2]; boolean valid; for (int i = 0; i < 2; i++) { valid = false; while (!valid) { try { System.out.println("Enter number " + (i + 1)); number[i] = Integer.valueOf(Keyboard.getString()).intValue(); valid = true; } catch (NumberFormatException e) { System.out.println("Invalid integer entered. Please try again."); } } } try { result = number[0] / number[1]; System.out.println(number[0] + " / " + number[1] + "=" + result); } catch (ArithmeticException e) { System.out.println("Second number is 0, cannot do division!"); } } } 运行结果如下: Enter number 1 abc Invalid integer entered. Please try again. Enter number 1 efg Invalid integer entered. Please try again. Enter number 1 143 Enter number 2 abc Invalid integer entered. Please try again. Enter number 2 40 143 / 40=3 ·160· Java语言程序设计(第3版) 5.2.3 生成异常对象 前面所提到的异常或者是由Java虚拟机生成,事实上,在程序中也可以自己生成异常 对象,即不是因为出错而产生异常,而是人为地抛出异常。 不论哪种方式,生成异常对象都是通过throw语句实现,例如: throw new ThrowableObject(); ArithmeticException e = new ArithmeticException(); throw e; 注意:生成的异常对象必须是Throwable或其子类的实例。 例5-6 生成异常对象举例。 程序如下: class ThrowTest { public static void main(String args[]) { try { throw new ArithmeticException(); } catch (ArithmeticException ae) { System.out.println(ae); } try { throw new ArrayIndexOutOfBoundsException(); } catch (ArrayIndexOutOfBoundsException ai) { System.out.println(ai); } try { throw new StringIndexOutOfBoundsException(); } catch (StringIndexOutOfBoundsException si) { System.out.println(si); } } } 运行结果如下: java.lang.ArithmeticException java.lang.ArrayIndexOutOfBoundsException java.lang.StringIndexOutOfBoundsException 5.2.4 自定义异常类 除了使用系统预定义的异常类外,用户还可以自己定义异常类,所有自定义的异常类都 必须是Exception的子类。一般的声明方法如下: 第5章 异常处理与输入输出流·161·