赞
踩
异常处理概述
异常(Exception)是程序在执行过程中所产生的问题。导致异常的产生的原因有很多种,包括:用户输入了无效的数据、找不到一个需要打开的文件、在通讯过程中网络连接断开或者JVM发生了内存溢出等等。
有些异常是由于用户的错误所导致的,有些是由程序员的错误导致的,有些则是由硬件设备的故障导致的。在本章中,我们将详细介绍不同类型的异常,以及在什么时候应该抛出一个异常,在什么时候应该捕获一个异常,如何编写和抛出自定义的异常。
为了更好地认识和理解Java语言中异常处理的工作机制,我们首先需要认识异常的三个种类:
虽然错误不是异常,但是错误在程序控制流程中发生时类似于异常。异常和错误都能够使得我们的应用程序崩溃。接下来我们将详细介绍异常的控制和处理。
要么直接异常处理,要么就给方法的调用者进行"异常声明"
不需要异常处理,也不需要异常声明”,让它报错,报错以后,我们程序员再通过“条件控制语句”对其进行控制即可
异常的控制流程
在Java语言中,异常(Exception)是被一个方法抛出的对象。当一个方法被调用时,这个方法被压入到内存的方法调用栈中。当一个方法抛出异常时,该方法从调用栈中被弹出,同时产生的异常对象抛给了栈中的前一个方法。例如,假如应用程序的main()方法在调用栈的底部,接着是method1()和method2()方法。如果method2()方法抛出一个异常,method2()方法就会从调用栈中被取出,同时异常抛给了下面的method1()方法。
对于这个异常的处理,method1()方法有三种选择:
其中的2,3都会使用抛出"异常"的方法,弹出方法调用栈.。
不论调用栈中有多少方法,程序控制流程都将继续在调用栈中向下执行。在调用栈中的每一个方法要么捕获并处理这个异常,要么捕获这个异常并再次抛出,或者什么都不做让异常进入下一个方法。
当这个异常到达调用栈的底部时会发生什么情况呢?如果Exception对象被抛给了main()方法,那么main()方法最好捕获该异常,否则程序就会终止。当一个异常到达调用栈的底部,如果没有方法来处理它,JVM将会崩溃,并且通知我们这个异常的详细情况。
Thowable类
在前面的小节中我们已经介绍了异常有三种不同的类型,但它们都拥有一个公共的父类:java.lang.Throwable。只有Throwable类型的对象能够被JVM抛出。Throwable类有两个子类:Exception和Error。图1是异常处理相关类的分类
Java语言中异常的继承层次关系就是基于这三个类。Error类是所有Java错误类的父类;Exception类是所有异常的父类,包括运行时异常和检查异常。
在这种继承层次关系中,运行时异常和检查异常根据适合的不同情况作出了进一步的区分。如果一个类是RuntimeException类的子类,那么这个子类代表了一个运行时异常。如果一个类是Exception的子类,但并不是RuntimeException的子类,那么这个类就是一个检查异常。
例如,ArrayIndexOutOfBoundsException和ArithmeticException是运行时异常,因为它们都是RuntimeException的子类。IOException和ClassNotFoundException是检查异常,因为它们都是Exception的子类。
图1 Java中异常的分类
Exception对象是Throwable类型的Java对象。当我们捕获到异常的时候,将获得一个指向Throwable类型对象的引用。每一个异常类都是不同的,并且拥有自己特定的方法。但是,由于所有的异常都继承于Throwable类,所以我们还可以在任何被捕获的异常上调用Throwable类的方法。
如下是Throwable类的一些方法的描述,更详细的方法描述请参考Java API文档:
Throwable类中的方法旨在帮助我们确定程序如何以及在何处发生了问题。在下一节,我们将介绍如何使用try/catch块捕获异常。
抓捕异常
在Java语言中,我们通常在一个方法中使用try和catch关键字来捕获异常。使用try/catch关键字的代码块把可能产生异常的代码“包围起来”,其中的代码也被称为“被保护的代码”,使用try和catch的语法形式如下所示:
| try { //被保护的代码 } catch(异常的名称 e1) { //捕获块 } |
catch语句包含我们想要捕获的异常的类型声明。如果在“被保护的代码”中发生异常,try块后面的catch块就会尝试对这个异常进行检查。如果发生的异常类型是在catch语句中所罗列出来的,那么异常对象就像方法的参数一样传递给catch块中。
需要注意的是,一个try/catch块并不能捕获一切。例如,如果我们想捕获一个NullPointerException异常,但是却发生了一个ArithmeticException异常,此时ArithmeticException就不能被捕获。
如下的try/catch块程序用于在打开一个文件时,试图捕获一个FileNotFoundException(文件没有找到)异常:
| try{ System.out.println("为读取文件而打开文件... "); file = new FileInputStream(fileName); }catch(FileNotFoundException f){ System.out.println("** 找不到文件" + fileName + " **"); f.printStackTrace(); return -1; } |
在程序运行时,如果找到了文件,则不会产生异常,catch块也将被跳过。如果找不到文件,FileInputStream类的构造器就会抛出一个FileNotFoundException异常。在实际的应用中,我们可以让用户尝试使用其他的文件名,但是这个catch异常处理块只是打印出调用栈跟踪信息并返回值-1,从而导致方法停止执行,并且使得程序控制流回到调用栈原来的方法中。
多个catch语句块
在上一个小节里,我们介绍了try/catch块的简单应用,但如果“被保护的代码”产生了多个不同类型的异常,catch块该如何处理呢?在Java的异常处理中,一个try块后可以跟随多个catch块。多个catch块的语法如下所示:
| try{ //被保护的代码 }catch(ExceptionType1 e1){ //Catch块 }catch(ExceptionType2 e2){ //Catch块 }catch(ExceptionType3 e3){ //Catch块 } |
在上面的语句中只列出了三个catch块,但是在单个try块后可以有任意多个catch块。如果在“被保护的代码”中有异常产生,那么这个异常就会抛给下面的第一个catch块。如果抛出的这个异常的数据类型匹配ExceptionType1,那么异常将在这里被捕获。如果不匹配,异常将继续向下传递给第二个catch块。这种情况将持续下去,直到异常被捕获或者通过所有的catch块。在这种情况下,目前的方法停止执行,并且异常向下抛出到调用栈前面的方法中。
2、在catch块的参数类型定义时,直接定义上层父类,采用动态多态的方式来解决问题
在定义多个catch块的时候,定义的顺序需要遵循从小到大的顺序
异常捕获与多态性
虽然一个try块后可以跟随多个catch块,但是catch块不能简单地以任意顺序列出。当异常发生的时候,catch块会依照它们排列的顺序被依次检查。由于多态性的存在,一个catch块有可能不会被检查到。
例如,如下的try块后面有两个catch块。第一个catch块用于捕获IOException,第二个catch块用于捕获FileNotFoundException。
| try{ file = new FileInputStream(fileName); x = (byte) file.read(); }catch(IOException i){ i.printStackTrace(); return -1; }catch(FileNotFoundException f) {//无效! f.printStackTrace(); return -1; } |
这段异常处理代码不能够通过编译,因为用于捕获FileNotFoundException的异常处理代码块是无法访问的。为什么呢?这是由于FileNotFoundException类是IOException类的子类,所以一个FileNotFoundException对象也是一个IOException对象(不要忘记多态性的is-a关系)。如果在try块中发生了FileNotFoundException,用于捕获IOException的代码块就将先被检查并执行。
由于所有的异常类都是Exception的子类,因此可以使用Exception来捕获所有的检查和运行时异常。
注:1、在定义多个catch块的时候,定义的顺序需要遵循从小到大的顺序
2、在try中某段代码发生了异常,那么try中是剩下的代码将不会继续执行,会直接去找到相应的catch处理。
异常处理及声明的规则
在Java语言中对于检查异常有一个严格执行的规则,这个规则被称为异常处理和声明规则。这个规则指出一个检查异常要么被处理,要么被声明。处理异常是指异常的捕获,而声明异常是指一个方法在方法签名时使用throws关键字,但是声明的异常在该方法中不会被处理。
必须注意的是:异常处理和声明规则不适用于运行时异常。如果程序导致了运行时异常,我们可以选择捕获这个异常,或者干脆忽略它使得程序崩溃。如前所述,在多数情况下,不要试图去捕获运行时异常,因为它们往往都是由于糟糕的代码设计所造成的。解决的办法是让运行时异常使程序崩溃,然后发现问题并解决。
我们想强调运行时异常与检查异常之间的区别,并解释为什么运行时异常没有遵守异常处理和声明的规则。例如,使用点运算符来访问对象的方法或属性,如果对象引用是null,那就可能产生一个NullPointerException。而NullPointerException类是RuntimeException的子类,因此它也是一个运行时异常。如果每次在使用点运算符的时候都要试图去捕获NullPointerException的话,那么代码中将会包含很多的try/catch块。幸好我们可以在代码中忽略潜在的运行时异常。
但是,假如我们试图打开一个文件,但是这个文件却并不存在,如果我们简单忽略文件不存在的这个事实,那么程序的其它部分如何避免受到影响呢?在Java语言中,我们不能够忽略像文件没找到这种情形。我们必须处理潜在的检查异常,否则Java代码就不能通过编译。
代码清单1所示的Lazy类包含了一个没有任何try/catch块的readOneByte()方法。由于readOneByte()方法没有遵循异常处理和声明规则,所以该类不能通过编译。
| //代码清单1 Lazy.java import java.io.*;
public class Lazy { private String fileName;
public Lazy(String name) { fileName = name; }
public byte readOneByte() { FileInputStream file = null; byte x = -1; System.out.println("正在为打开文件... "); file = new FileInputStream(fileName); System.out.println("刚刚打开的文件为:" + fileName); System.out.println("正在从文件中读取一个字节... "); x = (byte) file.read(); System.out.println("刚读了" + x + "个字节"); return x; } } |
编译器告诉我们readOneByte()方法要么必须捕获由new FileInputStream()语句可能产生的FileNotFoundException异常,要么对这个异常进行声明。对于read()方法可能产生的异常,编译器也做了同样的描述。
我们已经学习了如何使用try/catch块来处理异常。
声明异常
如果一个方法没有处理检查异常,那么该方法必须使用throws关键字来声明异常。关键字throws出现在方法签名的末尾。例如,下面的方法就声明抛出一个RemoteException异常:
| public void deposit(double amount) throws RemoteException |
一个方法也可以声明它抛出多个异常。在这种情况下,多个异常之间用逗号进行分隔。例如,下面的方法声明它抛出一个RemoteException异常和一个InsufficientFundsException异常:
| public void withdraw(double amount) throws RemoteException,InsufficientFundsException |
在一个方法中我们到底什么时候处理异常,什么时候声明异常呢?这要取决于我们的设计决策:是想在方法里面处理问题,还是想把问题传递给方法的调用者。
如果问题是与方法相关的,那么我们应该让方法来处理自己的问题。例如,假设我们走进银行存款(通过调用deposit()方法),但是银行出纳员的计算机出问题了。这显然不是我们的问题。在这种情况下,deposit()方法必须在不通告方法调用者的情况下处理该异常,并修复故障。
为什么deposit()方法要抛出一个异常呢?假如我们在存款和银行计算机系统故障中间,这肯定是我们的问题。在这种情况下,我们应该被通知存款事务不能继续下去。deposit()方法可以通过向我们抛出一个异常,来告诉我们事务失败。
一个常见的程序设计方法是让方法返回一个布尔值。如果存款成功了,返回一个true,否则返回一个false。那么,我们在这里为什么不采用这种方法呢?这是因为它并没有告诉我们到底出了什么错。假设我们去存钱,结果银行出纳员只是说“对不起,存不了”,那么我们就不知道为什么存不了。如果银行出纳员向我们抛出一个异常,那么我们就可以捕获该异常(这个异常是个Java对象),并判断到底是哪里出错了。
抛出一个异常看起来需要太多的开销。那么我们值得这样做么?异常处理是Java的一部分,与设计带来的好处相比,最小的开销不必关心。例如,如果我们试图取出超过存款账户余额的钱,只是用一个false返回值来告诉我取钱事务失败,并不能阻止我们透支。这样我们可能花了更多的钱,然后在透支通知函来的时候感到莫名其妙。我们可以告诉银行,我们不能检查withdraw()方法的返回值。
那么为什么在这种情况下,异常是一个更好的设计呢?这是因为如果一个异常是检查异常,我们就必须处理该异常。所以,如果withdraw()方法抛出了一个InsufficientFundsException(余额不足)异常,那么我们就必须在每次调用withdraw()方法时处理该异常。当然,一旦我们捕获了该异常,我们也做不了什么,我们还是会一直花我们没有的钱,但是我们现在不会再以不知道情况为借口,对银行说不知道透支了。与接口可以用于强制类的行为相似,异常处理或声明规则可以用于强制方法的调用者来处理潜在的问题,而不是简单地忽略它们。
当调用者调用这个方法的时候,必须用try/catch对它进行异常抓捕处理
1,当方法的调用者,需要知道"调用方法"的具体执行情况,而并非是"成没成功,失没失败"时, 我们就有必要针对"调用方法"进行异常声明,比如: transfer()想知道"张三给李四转钱5000"这个过程中,所有的执行方法的具体执行情况时.
抛出异常
我们可以通过使用throw关键字来抛出异常,这个异常可以是一个新的异常实例,也可以是一个我们刚刚捕获的异常。throw语句将导致当前代码立即停止执行,而且异常将被抛给调用栈中的前一个方法。
例如,下面的语句抛出一个新的ArrayIndexOutOfBoundsException(数组索引越界)异常对象,无效索引为5:
| throw new ArrayIndexOutOfBoundsException(5); |
我们也可以先实例化一个异常对象,然后再用另一条语句将其抛出:
| ArrayIndexOutOfBoundsException a = new ArrayIndexOutOfBoundsException(5); ...... throw a; |
finally关键字
关键字finally用于在try块后创建一个代码块。finally代码块总是会执行,不管异常是否发生。我们可以使用finally块来执行清理类型的语句,而不管被保护的代码中发生了什么。finally块出现在catch块的末尾,语法形式如下:
| try { //被保护的代码 }catch(ExceptionType1 e1) { //Catch块 }catch(ExceptionType2 e2) { //Catch块 }catch(ExceptionType3 e3) { //Catch块 }finally { //finally块总会执行。 } |
我们也可以写一个没有任何相关catch块、只有一个finally块的try块:
| try { //被保护的代码 }finally { //finally块总会执行。 } |
为什么要用finally块呢?假如我们对于一个已经打开的文件,不管该文件能够成功读取数据,还是因为某种原因读取数据失败,我们都要关闭该文件。这时候,用finally块就可以简化代码。例如:
| try { //尝试从文件读数据 } catch(IOException e) { //读数据失败 return; } finally { //关闭文件 } |
在上面的try/catch/finally代码块中,如果没有发生IOException异常,那么程序流程就跳过catch块,执行后面的finally块。如果发生了IOException异常,那么catch块就要执行,并且到达return语句。在方法返回之前,finally块将会执行。
方法重写与异常
在《继承》一章中,我们讨论了方法重写,即子类的方法可以重写父类的方法。在方法重写的学习中,我们提到一个规则就是:子方法不能抛出比父方法更多的异常。现在我们开始详细讨论这个规则。
为了更好地解释这个规则,我们来看一个示例。假设有一个类Parent,该类有一个connect()方法。connect()方法声明抛出java.io.IOException异常。
| import java.io.IOException;
public class Parent { public void connect() throws IOException { System.out.println("在父类的connect()方法内"); throw new IOException(); } } |
如下的Child1类继承了Parent类,并重写connect()方法。Child1中的connect()方法声明抛出一个java.net.SocketException异常。那么,这是一个有效的方法重写么?是的,因为SocketException是IOException的子类,所以SocketException是一个比IOException“小”的异常。
| import java.net.SocketException;
public class Child1 extends Parent { public void connect() throws SocketException { System.out.println("在Child1的 connect()方法内"); throw new SocketException(); } } |
我们可以重写一个抛出一个异常的父类方法,并在子类方法中不声明任何异常。例如,如下的Child2类继承Parent类,并重写connect()方法。子类方法没有声明任何异常,这显然是比IOException异常“小”。
| public class Child2 extends Parent { public void connect() { System.out.println("在Child2的 connect()方法内"); } } |
现在我们再来看一个不能正常工作的示例。如下的Child3类继承Parent,并试图重写connect()方法。在编译时会产生一个编译器错误,因为Child3中的connect()方法声明它抛出了Exception异常,这个异常比IOException异常“大”。
| public class Child3 extends Parent { public void connect() throws Exception { // 不能通过编译! System.out.println("在Child3类的connect()方法内"); throw new Exception(); } } |
当重写一个方法时为什么会有这个规则呢?
子类中的方法在重写父类中的方法时,抛出的异常不能比父类中声明的异常更多或更大。这看起来像是Java中一个古怪的规则,但是它却有特定的原因。如果允许子类方法抛出更大的异常,那么我们实际上就创造了一个让检查异常避开异常处理或声明的规则限制的条件。
下面,我们用一个简单的示例来进行说明。假如我们有如下Parent类:
| public class Parent { public void connect() { System.out.println("在Parent类的connect()方法内"); } } |
我们注意到connect()方法没有声明任何异常。如下的Child类继承了Parent类,并重写connect()方法:
| public class Child extends Parent { public void connect() throws java.io.IOException { System.out.println("在Child类的connect()方法内"); throw new java.io.IOException(); } } |
Child类不能通过编译,因为它的connect()方法声明了一个IOException异常,而被重写的Parent类的connect()方法并没有声明任何异常。
现在我们假定,异常和重写方法的规则不存在,并且Child类可以成功地被编译。那么,因为多态性,下面的语句就是有效的:
| Parent p = new Child(); |
引用p是Parent类型的,但是对象是Child类型。这是有效的,因为一个Child类型对象是一个(is a)Parent类型对象。现在,考虑如下的程序,用引用p来调用connect()方法:
| public class Test { public static void main(String [] args) { Parent p = new Child(); p.connect(); } } |
该程序可以编译通过,因为Parent类有connect()方法。但是,因为动态绑定机制,那么在运行期到底执行哪个方法呢?不是Parent中的connect()方法,而是Child中重写的方法!Child中的connect()方法抛出IOException异常,但是谁来捕获它呢?没有人!我们刚刚创造了一个检查异常不被检查的状况。编译器会认为它调用的是Parent中的没有声明任何异常的connect()方法。但是,在运行时,执行的是Child中抛出IOExeption异常的connect()方法。
在这种情况下,编译器是无法预知异常处理或声明规则的。这就是为什么子类的方法在重写父类的方法时,不能抛出比父类方法“更多”异常的原因。如果抛出了更多的异常,那么检查异常就永远不能被捕获到。
用户自定义异常
在Java中,我们可以创建自定义异常。实际上,因为Java被设计的方式,我们被鼓励来编写自定义的异常,以代表在我们的类中会出现的问题。在编写自定义异常类时,必须牢记如下几点:
我们肯定不会编写一个直接继承Throwable类的类,因为此后这个类既不是检查异常,也不是运行时异常。大多数用户自定义的异常类都被设计为检查异常,因而会继承Exception类。但是,如果我们想编写一个不想让用户处理或声明的异常,就应该通过继承RuntimeException类来使它成为一个运行时异常。
如下的InsufficientFundsException类是一个继承Exception类的用户自定义异常,它是检查异常。异常类与其它类一样,包含有字段和方法。在这个示例中,异常类的名称(余额不足异常)已经很好地解释了其含义,但是我们添加了一个字段来存储不足的余额数量,并用一个访问器方法来查看该字段。
| public class InsufficientFundsException extends Exception { private double amount;
public InsufficientFundsException(double amount) { this.amount = amount; }
public double getAmount() { return amount; } } |
总结
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。