Java 异常处理

来源:互联网 发布:北京软件测试工资 编辑:程序博客网 时间:2024/05/22 12:23
 用异常来处理错误
我们总是试图避免在软件程序中错误的发生,但是它的存在却是不幸的实事。无论怎样,如果你能适当的处理错误,将会极大的改善程序的可读性、可靠性以及可维护性。Java编程语言使用异常来处理错误。这章主要讲述在什么时候和怎样使用异常来处理错误。
什么是异常?
Java编程语言使用异常机制为程序提供了错误处理的能力。一个异常是在程序执行期间发生的一个事件,它使用正常的指令流中断。
捕获或指定需求
这段讲述捕获和处理异常。在讨论中包括:try,catch和finally程序块,还有异常链和日志。
怎样抛出异常
这段讲述异常抛出语句和Throwable类以及它的子类。
有关未检查的异常争论
因为Java编程语言不要求捕获或指定运行时异常或错误的方法,所以程序员会被诱导来编写只抛出运行时异常代码,在这段中我们会解释为什么不应该被诱导。
异常的好处
在这段中,你将会学到更多的使用异常来管理错误优于传统的错误管理技术方面的知识。
什么是异常?
异常(exception)应该是异常事件(exceptional event)的缩写。
异常定义:异常是一个在程序执行期间发生的事件,它中断正在执行的程序的正常的指令流。
当在一个方法中发生错误的时候,这个方法创建一个对象,并且把它传递给运行时系统。这个对象被叫做异常对象,它包含了有关错误的信息,这些信息包括错误的类型和在程序发生错误时的状态。创建一个错误对象并把它传递给运行时系统被叫做抛出异常。
一个方法抛出异常后,运行时系统就会试着查找一些方法来处理它。这些处理异常的可能的方法的集合是被整理在一起的方法列表,这些方法能够被发生错误的方法调用。这个方法列表被叫做堆栈调用(call stack),调用方式如下图所示(图片参见附件)

运行时系统搜寻包含能够处理异常的代码块的方法所请求的堆栈。这个代码块叫做异常处理器,搜寻首先从发生的方法开始,然后依次按着调用方法的倒序检索调用堆栈。当找到一个相应的处理器时,运行时系统就把异常传递给这个处理器。一个异常处理器要适当地考滤抛出的异常对象的类型和异常处理器所处理的异常的类型是否匹配。异常被捕获以后,异常处理器关闭。如果运行时系统搜寻了这个方法的所有的调用堆栈,而没有找到相应的异常处理器,如下图所示,运行进系统将终止执行。(图片参见附件)

使用异常来管理错误比传统的错误管理技术有一些优势,你可以“异常的优势”一节学到更多的知识

异常管理的优势
你已经读了有关什么是异常以及怎样使用它们的内容,现在是学习在你的程序中使用异常的好处的时候了。
优势1:把规则代码与错误处理代码分离
异常处理规定把错误发生时所要的细节工作与程序的主逻辑代码分离。在传统程序中,错误的发现、报告以及处理经常使得代码混乱。例如,思考下面的伪代码,这是一个把整个文件读入内存的方法。
readFile {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
}
第一眼看上去,这个函数似乎很简单,但是它却忽略了所发生下面这些错误的可能。
1、    如果不能打开文件,会发生什么?
2、    如果不能判定文件的大小,会发生什么?
3、    如果没有足够的内存,会发生什么?
4、    如果读取失败,会发生什么?
5、    如果文件不能关闭。会发生什么?
要处理这些信息,readFile函数必须用更多的代码来做错误发现、报告和处理工作。这个函数看上去可能象这样:
errorCodeType readFile {
    initialize errorCode = 0;
    open the file;
    if (theFileIsOpen) {
        determine the length of the file;
        if (gotTheFileLength) {
            allocate that much memory;
                 if (gotEnoughMemory) {
                    read the file into memory;
                      if (readFailed) {
                          errorCode = -1;
                      }
                  else {
                    errorCode = -2;
                }
        } else {
                    errorCode = -3;
            }
            close the file;
            if (theFileDidntClose && errorCode == 0) {
                   errorCode = -4;
            } else {
                    errorCode = errorCode and -4;
            }
        } else {
            errorCode = -5;
        }
    return errorCode;
}

有如此多的错误发现、报告和返回,使得初的7行代码被埋没在混乱的错误代码之中。更严重的是,代码的逻辑流已经没有了,这样使得它很难说明代码是否正在做着正确的事情:如果函数在分配内存过程失败,文件真得的被关闭了吗?甚至更难保证在三个月之后,你编写的这段代码继续做正确的事情。

异常处理使你能够编写代码的主工作流并且在别的地方来处理异常信息。如果readFile函数使用异常处理来代替传统的错误管理技术,它应该像如下所示的代码这样:
readFile {
    try {
        open the file;
        determine its size;
        allocate that much memory;
        read the file into memory;
        close the file;
    } catch (fileOpenFailed) {
       doSomething;
    } catch (sizeDeterminationFailed) {
        doSomething;
    } catch (memoryAllocationFailed) {
        doSomething;
    } catch (readFailed) {
        doSomething;
    } catch (fileCloseFailed) {
        doSomething;
    }
}

注意:异常处理不会节省错误的发现、报告、处理的工作量,但是它们能够帮助你更有效的组织代码。

优势2:向调用堆栈上层传递错误
异常处理的第二个优势是向方法的调用堆栈上层传递错误报告的能力。假如readFile方法是主程序调用的一系列嵌套方法中的第四个方法:方法1调用方法2,方法2调用方法3,方法3调用readFile,代码结构如下所示:
method1 {
    call method2;
}
method2 {
    call method3;
}
method3 {
    call readFile;
}
还假如method1是唯一的能够处理readFile方法中所可能发生的错误的方法,那么传统的错误处理技术会强制method2和method3来传递通过readFile调用堆栈所返回的错误代码,直到错误代码传递到method1-因为只有method1能够处理这些错误,其代码结构如下所示:
method1 {
    errorCodeType error;
    error = call method2;
    if (error)
        doErrorProcessing;
    else
        proceed;
}
errorCodeType method2 {
    errorCodeType error;
    error = call method3;
    if (error)
        return error;
    else
        proceed;
}
errorCodeType method3 {
    errorCodeType error;
    error = call readFile;
    if (error)
        return error;
    else
        proceed;
}

回忆一下,Java运行时环境搜寻调用堆栈来查找任意的处理特殊的异常的方法。一个方法能够抛出它内部的任何异常,所以允许一个上层调用堆栈的方法来捕获它。因此只有处理相关错误的方法来处理发现的错误,代码结构如下所示:
method1 {
    try {
        call method2;
    } catch (exception e) {
        doErrorProcessing;
    }
 
}
method2 throws exception {
    call method3;
}
method3 throws exception {
    call readFile;
}

无论怎样,就像伪代码所展示的那样,躲避异常需要中间方法做一些工作。任意被检查到的由内部方法的抛出的异常必须在这个方法的throws子句中被指定。

优势3:分组和区分错误类型

因为所有在程序内部抛出的异常都是对象,异常的分组或分类是类继承的自然结果。在Java平台中一组相关异常类的例子是在java.io中定义的IOException和它的子类。IOException是最普通的IO异常管理类,并且它描述了在执行I/O操作时所发生的任意的错误类型。它的子类描述了一些特殊的错误。例如,FileNotFoundException异常类代表不能在本地磁盘上找到一个文件。
一个方法能够编写特殊的异常处理器,使它能够处理非常特殊的异常。FileNotFoundException异常类没有子类,因此下面的异常处理器只能处理一种异常类型:
catch (FileNotFoundException e) {
    ...
}

一个方法能够基于它的分组或通过在catch子句中所指定的任何异常的超类的一般类型来捕获异常。例如,要捕获所有的I/O异常,而不管它们的具体类型,就可以在异常处理器中指定一个IOException参数:
catch (IOException e) {
    ...
}

这个处理器将捕获所有的I/O异常,包括FileNotFoundException,EOFException等等。你能够通过查询传递给异常处理器的参数找到发生错误的详细信息。例如,打印堆栈执行路线:
catch (IOException e) {
    e.printStackTrace();            // output goes to Sytem.err
    e.printStackTrace(System.out);  // send trace to stdout
}

你甚至可以创建一个能够处理任意类型的异常的异常处理器:
catch (Exception e) {    // a (too) general exception handler
    ...
}

Exception类是Throwable类结构中的顶级类,因此,这个处理器将捕获除了那些被特定处理器捕获的异常以外的异常。你可能想你的程序是否都是这种处理异常的方法,例如,为用户打印错误消息并且退出。
但是,在大多数情况下,你需要异常处理器来尽可能的处理精确一些。原因是在处理器决定最好的恢复策略之前,必须做第一件是判断发生异常的类型是什么。在没有捕获特定错误的情况下,处理器必须有效的提供任意的可能性。Exception 处理器是最一般的异常处理器,使用这个处理器使得代码捕获和处理更多的程序员没有预料到的错误倾向,从而使得处理器没有目的性。

象我们展示的一样,你能够创建异常组,并且用一般化的方式来处理异常,或者使用特定异常类型来区分异常并且用精确的方式来处理异常。

捕获和处理异常

这段向说明怎样使用异常处理器的三个组成部分-try,catch和finally块来编写异常处理器。这段最后举一个例子,并且分析在不同的情况下发生了什么。

下面的例子定义和实现了一个叫做ListOfNumbers的类。在类的构造器中,ListOfNumbers创建了一个Vector,它包含了从0到9的十个连续的整数。ListOfNumbers类也定义了一个叫writeList的方法,这个方法把这个数字列表写入一个叫做OutFile.txt的文本文件中。这个例子使用了在java.io中定义的输出类,这个类在I/O:读写这一章中介绍。

// 注意: 这样的设计,这个类不会被编译
import java.io.*;
import java.util.Vector;

public class ListOfNumbers {
 
    private Vector victor;
    private static final int SIZE = 10;

    public ListOfNumbers () {
        victor = new Vector(SIZE);
        for (int i = 0; i < SIZE; i++) {
            victor.addElement(new Integer(i));
        }
    }

    public void writeList() {
        PrintWriter out = new PrintWriter(
                new FileWriter("OutFile.txt"));

        for (int i = 0; i < SIZE; i++) {
            out.println("Value at: " + i + " = " +
                victor.elementAt(i));
        }

        out.close();
    }
}

这个例子中的第一行黑体字部分代码调用了一个构造器,这个构造器初始化一个文件输出流。如果这个文件不能被打开,这个构造器会抛出一个IOException异常。第二行黑体字部分代码调用一个Vector类的elementAt方法,如果它的参数值太小(小于零)或太大(大于Vector中当前所包含的元素数),那么它会抛出一个ArrayIndexOutOfBoundsException异常。

如果试图编译ListOfNumbers类,编译会打印一个有关被FileWrite构造器所抛出的异常的错误消息。这是因为构造所抛出的IOException异常是一个编译检查性异常,被elementAt方法抛出的ArrayIndexOutOfBoundsException异常是一个运行时异常,而Java编程语言只要求程序处理编译检查性异常,所以你只能获取一个错误消息。

现在随着对ListOfNumbers类的熟悉,并且知道异常是在程序中的什么地方抛出的,那么你就可以准备把异常处理器编写到catch块来处理那些异常。

怎样抛出异常
在你能够捕捉一个异常之前,在程序中的某个地方必须有抛出这个异常的代码在在。任何代码都可以抛出异常:它们可以你自己的代码,也可以是来自于别人所写的包中的代码(例如与Java平台一起提供的程序包),或者是Java运行时环境。不管抛出什么样的异常,都要使用throw语句把异常抛出。

你可能已经注意到,Java平台提供了各种的异常类。所有的这些类都是Throwable类的子类,并且它们都允许程序来区分在程序执行期间所发生的各种类型的异常。

你也可以创建自己的异常类来描述你编写的类中所发生的问题。实际上,如果你是一个程序包的开发人员,你可能必须创建你自己的异常类的集合,以便于让你的用户来区分在你的程序包中发生的错误是来自己于Java平台还是其它的包。

你也可以创建异常链,异常链在Java Standard Edition 1.4中被引入。更多的信息,请看“异常链”这一节。

“throw”语句
所有的方法都使用“throw”语句来抛出一个异常。Throw语句需要一个单独throwable对象,这个对象是任意Throwable类的子类。如下类所示:
throw someThrowableObject;

让我们在程序的上下文中来看一下throw语句。下面的pop方法把来自于一个公共堆栈中的一个执行类给删除。这个方法从堆栈上面的元素,并且返回被删除的对象。
public Object pop() throws EmptyStackException {
    Object obj;

    if (size == 0) {
        throw new EmptyStackException();
    }

    obj = objectAt(SIZE - 1);
    setObjectAt(SIZE - 1, null);
    size--;
    return obj;
}

pop方法检查堆栈上是否有元素。如果堆栈是空的(也就是说它的尺寸等于0),pop方法就会实例化一个新的EmptyStackException对象(它是java.util中的一个成员),并且抛出它。在这章的后面一节会解释怎样创建自己的异常类。对于现在,你所需要记住的是你只能抛出继承于java.lang.Throwable类的对象。

注意,pop方法的声明中包含了一个throws子句。EmptyStackException是一个检查性异常,并且pop方法没有捕捉这个异常。因此,这个方法必须使用throws子名来声明它所抛出的异常的类型。

Throwable 类和它的子类

继承Throwable类的对象包括直接子类(直接继承于Throwable类的对象)和间接子类(继承于Throwable类的子类的对象)。下图说明了Throwable类的层次关系和最主要的一些子类。象你看到的一样,Throws有两个直接的子类:Error类和Exception类。

 

Error类
当在Java虚拟机中发生动态连接失败或其它的定位失败的时候,Java虚拟机抛出一个Error对象。典型的简易程序不捕获或抛出Errors对象。

Exception类
大多数程序都抛出或捕获衍生于Exception类的对象。一个异常表明发生了一个问题,但它不是严重的系统问题。你编定的大多数程序将会抛出或捕获Exceptions对象(而不是Errors对象)。

在Java平台中Exception类有许多已经定义了的子类。这些子类说明所发生的异常的各种类型。例如,IllegalAccessException异常类说明了不能找到一个特殊的方法;NegativeArraySizeException异常类说明程序试图创建一个带有负尺寸的数组。

有一个特殊的Exception子类:RuntimeException。这个子类是在程序运行期间在Java虚拟机内部所发生的异常。例如NullPointerException类就是一个运行时异常类,在一个方法试图通过一个null引用来访问一个对象的成员时会发生这个异常。在Unchecked Exceptions---The Controversy这一节中,我们会讨论为什么典型的程序不应该抛出运行时异常或RuntimException类的子类异常对象。

有争议性的未被检查的异常
因为Java编程语言不要求方法一定要捕获或列出运行异常或错误,所以程序员就可能被误导,编写只抛出运行时异常或者使所有的异常子类都继承于RuntimException的代码,这两种快捷方式允许程序员编写不用为编译错误而操心的代码,并且也不费神去指定或捕获任何异常。尽管这种方法看上去对程序员很方便,但它回避了捕获或指定必要的东西的意图,并且可能使使用你的类的程序员发生错误。

为什么设计者决定强制一个方法指定所有的在它的范围内可能被抛出的未检查异常呢?被一个方法抛出的任何异常都是方法公共编程接口的一部分。方法的调用者必须知道有关这个方法所抛出的异常,以便他们能够针对这些异常决定做什么。这些异常是编写方法的接口差不多,有它们的参数和返回值。

接下来你的问题可能是:如果它是这么好的说明了一个方法的API,包括它能抛出的异常,那么为什么不也指定运行时异常呢?运行时异常描述的问题是一个设计问题的结果,并且,API的客户代码不能期望从出错的地方来恢复程序执行或用一些方法来处理它们。这些问题包括算法异常(例如被零除),指针异常(例如通过一个空的引用来访问一个对象),以及索引异常(例如试图通过一个越界的索引来访问一个数组)。运行时异常可能在程序的任何地方发生,并且在一个典型的程序中可能有很多,因此,在每个方法中不得不添加运行异常来降低程序的透明度,这样,编译器不要求你指定或捕获运行时异常(尽管你可以)。

抛出运行时异常(RuntimeException)的一个公共案例就是在用户调用了一个错误的方法的时候。例如,一个方法检查它的参数是否有效,如果一个参数是空(null),那么这个方法就可能抛出一个NullPointerException异常,这是一个不检查异常。

一般来说,不抛出运行时异常(RuntimeException)或不创建一个运行时异常(RuntimeException)的子类的原因是:人你不想为指定你的方法所能抛出的异常而操心。

一个使用异常的方针是:如果客户能够被期望从一个异常中得到恢复,那么就要使用检查性异常。如果一个客户对于从异常中恢复的程序不能做任何事,那么就可以使用不检查性异常。
原创粉丝点击