3.6 异常处理
Java是一个讲究安全性的语言。任何可能在程序运行过程中产生打断程序正常执行的事件都有用来保护的陷阱。Java异常处理机制提供了去检查及处理产生各种错误、异常事件的方法。
3.6.1 异常概述
1.异常的概念
用Java的术语来说,在程序运行过程中发生的、会打断程序正常执行的事件称为异常(Exception),也称为例外。程序运行过程中可能会有许多意料之外的事情发生,例如,零用做了除数(产生算术异常ArithmeticException)、在指定的磁盘上没有要打开的文件(产生文件未找到异常FileNotFoundException)、数组下标越界(产生数组下标越界异常ArrayⅠndexOutOfBoundsException)等。对于一个实用的程序来说,处理异常的能力是一个不可缺少的部分,它的目的是保证程序在出现任何异常的情况下仍能按照计划执行。
下面先来看一个Java系统对异常的处理的例子。
【例3.25】Java系统对异常的处理。
public class SystemException{ public static void main(String args[]){ int a = 68; int b = 0; System.out.println(a / b); // 0用做了除数 } }
程序运行结果为:
Exception in thread "main" java.lang.ArithmeticException: / by zero at SystemException.main(SystemException.java:5)
屏幕显示的信息指明了异常的类型:ArithmeticException: / by zero(算术异常/用0除)。在这个程序中,未进行程序的异常处理。这是因为除数为零是算术异常,它属于运行时异常(RunTimeException),通常运行时异常在程序中不做处理,Java运行时系统能对它们进行处理。Java语言本身提供的Exception类已考虑了多种异常的处理。
2.Java对异常的处理机制
Java异常处理机制提供了一种统一和相对简单的方法来检查及处理可能的错误。例如,在程序中出现了除以零的错误时,即抛出(throw)一个ArithmeticException异常实例,异常处理程序捕获(catch)这个异常实例,并对它进行处理。如果系统找不到相应的异常处理程序,则由Java默认的异常处理程序来处理,即在输出设备上输出异常信息,同时程序停止运行。有时也可以在程序中写一些自己的例程来处理异常;也可以将错误交给Java系统去处理,从而可以使得程序安全地退出。
3.异常类的层次和主要子类
Java用面向对象的方法处理异常,所有的异常都是Throwable类的子类生成的对象。所有的异常类都是Throwable类的后代。Throwable类的父类是Java的基类(Object),它有两个直接子类:Error类和Exception类。运行时异常RuntimeException类是Exception类的子类。只有Throwable类的后代才可以作为一个异常被抛出。Java语言的异常处理的主要子类见表3.1~表3.3。
4.异常类的方法和属性
(1)异常类的构造方法
构造方法是一类方法名与类名相同的方法,用于创建并初始化该类的对象。Exception类有四个重载的构造方法,常用的两个构造方法为:
· public Exception()创建新异常。
· public Exception(String message); 用字符串参数message描述异常信息创建新异常。
(2)异常类的方法
Exception类常用的方法有:
· public String toString()返回描述当前异常对象信息的字符串。
· public String getMessage()返回描述当前异常对象的详细信息。
· public void printStackTrace()在屏幕上输出当前异常对象使用堆栈的轨迹,即程序中先后调用了哪些方法,使得运行过程中产生了这个异常对象。
【例3.26】运行时异常产生时的信息显示。
class ExceptionDemo{ public static void main(String[] args) { String s = "123.45"; methodA(s); } static void methodA(String s) {
Integer i = new Integer(s); System.out.println(i); } }
程序运行结果如下:
Exception in thread "main" java.lang.NumberFormatException: 123.45 at java.lang.Integer.parseInt(Integer.java:438) at java.lang.Integer.<init>(Integer.java:570) at MyClass.methodA(MyClass.java:7) at MyClass.main(MyClass.java:4)
本程序运行结果说明程序运行时产生一个NumberFormatException数值格式异常。在用构造方法Ⅰnteger将一个字符串转换为Ⅰnteger数据时,参数字符串格式不对,所以产生了这个运行时异常,Java系统(即系统调用方法printStackTrace())将调用堆栈的轨迹打印了出来。输出的第一行信息也是toString()方法输出的结果,对这个异常对象进行简单说明。其余各行显示的信息表示了异常产生过程中调用的方法,最终是在调用 Ⅰnteger.parseⅠnt()方法时产生的异常,调用的出发点在main()方法中。
3.6.2 异常处理
在Java语言中,异常有几种处理方式:
(1)可以不处理运行时异常,由Java虚拟机自动进行处理。
(2)使用try-catch-finally语句捕获异常。
(3)通过throws子句声明抛弃异常,还可以自定义异常,用throw语句来抛出它。
1.运行时异常
运行时异常是Java运行时系统在程序运行中检测到的,可能在程序的任何部分发生,而且数量可能较多,如果逐个处理,工作量很大,有可能影响程序的可读性及执行效率,因此,Java编译器允许程序不对运行时异常进行处理,而将它交给默认的异常处理程序,一般的处理方法是在屏幕上输出异常的内容以及异常的发生位置。如例3.25 和例3.26 所示。当然,在必要的时候,也可以声明、抛出、捕获运行时的异常。
表3.1 Error类
表3.2 Exception类
表3.3 RuntimeException类
2.try-catch-finally语句
在Java语言中,允许自己来处理异常。Java语言提供try-catch-finally语句来捕获和处理异常,该语句的格式如下:
try{ 语句 // 可能产生异常的语句 }catch(Throwable-subclass e){ // 异常参数 语句 // 异常处理程序 }catch(Throwable-subclass e){ // 异常参数 语句 // 异常处理程序 }…
finally{ 语句 }
try语句块中是可能产生异常对象的语句,一旦其中的语句产生了异常,程序即跳到紧跟其后的第一个catch子句。try程序块之后的catch子句可以有多个,也可以没有。
catch子句中都有一个代表异常类型的形式参数,这个参数指明了这个catch程序块可以捕获并处理的异常类型。若产生的异常类型与catch子句中声明的异常参数类型匹配,则执行这个catch程序块中的异常处理程序。匹配的意思是指异常参数类型与实际产生的异常类型一致或是其父类。若不匹配则顺序寻找下一个catch子句,因此catch语句的排列顺序应该从特殊到一般,否则,放在后面的catch语句将永远执行不到。
也可以用一个catch语句处理多个异常类型,这时它的异常类型参数应该是这多个异常类型的父类。
若所有catch参数类型与实际产生的异常类型都不匹配,则标准异常处理程序将被调用,即在输出设备上输出异常信息,同时程序停止运行。
【例3.27】捕获除数为零的异常,并显示相应信息。
class ExceptionDemo1{ public static void main(String args[]) { int d, a; try { // 监控可能产生异常的代码块 d = 0; a = 68 / d; System.out.println("本字符串将不显示。"); } catch (ArithmeticException e) { // 捕获divide-by-zero错误 System.out.println("产生用零除错误。"); } System.out.println("在捕获语句后。"); } }
程序运行结果如下:
产生用零除错误。 在捕获语句后。
【例3.28】多个catch子句的try语句。
class ExceptionDemo2 { public static void main(String args[]) { try { int a = args.length; System.out.println("a = " + a); int b = 42 / a; int c[] = { 1 }; c[4] = 99; }catch(ArithmeticException e) { // 捕获算术运算异常 System.out.println("Divide by 0: " + e); }catch(ArrayIndexOutOfBoundsException e) { // 捕获数组下标越界异常 System.out.println("Array index oob: " + e);
} System.out.println("After try/catch blocks."); } }
程序运行结果如下:
a = 0 Divide by 0: java.lang.ArithmeticException: / by zero After try/catch blocks.
catch子句中异常参数的声明原则是从特殊到一般,若将一般(范围宽)的异常参数放到了前面,特殊(范围窄)的异常参数放到了后面,编译系统会指出下列错误:
… : catch not reached.
这是提示后面的catch子句根本不会被执行到,因为它能捕获的异常已经被前面的catch子句捕获了。
try语句中的finally子句的作用是说明必须执行的语句,无论try程序块中是否抛出异常,finally程序块中的语句都会被执行到。
【例3.29】有finally子句的try语句。
class ExceptionDemo3{ public static void main(String args[]){ try{ int x=0; int y=20; int z=y/x; System.out.println("y/x的值是: "+z); }catch(ArithmeticException e){ System.out.println("捕获到算术异常:" + e); }finally{ System.out.println("执行到finally块内!"); try{ String name = null; if(name.equals("张三")){ // 字符串比较,判断name是否为"张三" System.out.println("我的名字叫张三"); } }catch(Exception e){ System.out.println("又捕获到异常:" + e); }finally{ System.out.println("执行到内层finally块内!"); } } } }
程序运行结果如下:
捕获到算术异常:java.lang.ArithmeticException:/ by zero 执行到finally块内! 又捕获到异常:java.lang.NullPointerException
执行到内层finally块内!
在Java语言中,try-catch-finally语句允许嵌套。本例中是将内层的try嵌套在外层的finally块内。在程序执行到外层的try程序块时,由于分母为零而产生了算术异常,所以程序转移到第一个catch块。该catch捕获了这个算术异常,并进行了处理,之后程序转向必须执行的外层的finally程序块。因为该finally块产生空指针异常(一个null字符串和字符串“张三”进行比较),所以内层catch子句再次捕获到异常,最后程序转移到内层的finally程序块。
finally块还可以和break、continue和return等流程控制语句一起使用。当try程序块中出现了上述这些语句时,程序必须先执行finally程序块,才能最终离开try程序块。
【例3.30】break与finally的联用。
class BreakAndFinally{ public static void main(String args[]){ for(;;) try{ System.out.println("即将退出循环了!"); break; }finally{ System.out.println("finally块总要被执行到!"); } } }
程序运行结果如下:
即将退出循环了! finally块总要被执行到!
3.throw语句和throws子句
throw语句可使用户自己根据需要抛出异常。throw语句以一个异常类的实例对象作为参数。一般的形式是:
方法名(arguments)throws异常类{ ... throw new异常类(); }
new运算符被用来生成一个异常类的实例对象。例如:
throw new ArithmeticException();
包含throw语句的方法要在方法头参数表后书写throws子句。它的作用是通知所有要调用此方法的其他方法,可能要处理该方法抛弃的那些异常。若方法中的throw语句不止一个,throws子句应指明抛弃的所有可能产生的异常。
通常使用throws子句的方法本身不处理本方法中产生的异常,声明抛弃异常使得异常对象可以从调用栈向后传播,直到有合适的方法捕获它为止。对未用throw语句产生系统异常的方法,也可以使用throws子句来声明抛弃异常。例2.9 和例2.10 就是这种情况,在main()方法中不对异常进行处理。
为了能捕获throw抛出的异常,应在try块中调用包含throw语句的方法。
throw语句和throws子句的使用见下例。
【例3.31】throw语句和throws子句的使用。
class ThrowDemo{ void inException()throws ArithmeticException{ throw new ArithmeticException(); } public static void main(String args[]){ ThrowDemo s = new ThrowDemo(); try{ s.inException(); }catch(Exception e){ System.out.println("异常来了:" + e); } } }
程序运行结果如下:
异常来了: java.lang.ArithmeticException
程序中通过throw语句抛出一个算术异常,包含throw语句的inException ()方法头中加入了throws子句,它指明要抛弃算术异常。inException ()方法的调用发生在try块中以捕获抛出的异常,捕获异常的情况与前面见过的用零除情况一样。
需注意的是:throw语句一般应放入分支语句中,表示仅在满足一定条件后才被执行,而且throw语句后不允许有其他语句,否则将出现编译错误信息:unreachable statement。
4.创建自己的异常
在例3.31 中,用throw抛出的异常类是系统提供的算术异常类。Java语言还允许定义用户自己的异常类,从而实现用户自己的异常处理机制。使用异常处理使得自己的程序足够健壮以从错误中恢复。定义自己的异常类要继承Throwable类或它的子类,通常是继承Exception类。
【例3.32】设计自己的异常。从键盘输入一个double类型的数,若不小于0.0,则输出它的平方根,若小于0.0,则输出提示信息“输入错误!”。
import java.io.*; import javax.swing.JOptionPane; class MyException extends Exception{ void test(double x)throws MyException{ if(x < 0.0)throw new MyException(); // 条件成立时,执行throw语句 else System.out.println(Math.sqrt(x)); } public static void main(String args[])throws IOException{ MyException n = new MyException(); try{ String s = JOptionPane.showInputDialog("请输入一个实数:"); n.test(Double.parseDouble(s)); }catch(MyException e){ System.out.println("输入错误!"); }
} }
程序的两次运行结果如下:
(1)求输入实数的平方根。请输入一个实数:123.5
11.113055385446435
(2)求输入实数的平方根。请输入一个实数:-789
输入错误!
在本程序中,自己的异常类通过extends继承了Exception异常类。在test()方法中,用throw语句指定了可能抛出的异常,这个语句在调用test()方法时,参数小于0时被执行,产生异常并抛出。