您的位置: 网站首页 > 程序开发 > Java程序设计 > 第8章 异常处理 > 【8.3 Java的异常处理】

8.3 Java的异常处理

 

8.3  Java的异常处理

异常的处理主要包括捕获异常、程序流程的跳转和异常处理语句块的定义等。当一个异常被抛出时,应该有专门的语句来捕获这个被抛出的异常对象,这个过程被称为捕获异常。当一个异常类的对象被捕获后,用户程序就会发生流程的跳转,系统中止当前的流程而跳转至专门的异常处理语句块,或直接跳出当前程序和Java虚拟机,退回到操作系统。

8.3.1  异常类说明

Java中所有的异常都由类来表示。所有的异常都是从一个名为Throwable的类派生出来的。因此,当程序中发生一个异常时,就会生成一个异常类的某种类型的对象。Throwable类有两个直接子类:ExceptionError

Error类型的异常相关的错误发生在Java虚拟机中,而不是在程序中。错误类(Error)定义了被认为是不能恢复的严重错误条件。在大多数情况下,当遇到这样的错误时,建议让该程序中断。这样的异常超出了程序可控制的范围。

由程序运行所导致的错误由Exception类来表示,该异常类定义了程序中可能遇到的轻微的错误条件。可以编写代码来处理这样的异常并继续执行程序,而不是让程序中断。它代表轻微的可以恢复的故障。接收到异常信号后,调用方法捕获抛出的异常,在可能的情况下再恢复回来,这样程序员可以通过处理程序来处理异常。

Java中的异常类具有层次组织,其中Throwable类是Error类(错误类)和Exception类(异常类)的父类,Throwable类是Object类的直接子类。

异常类(java.lang.Exception)继承于java.lang.Object类中的java.lang.Throwable类。异常可分为执行异常(Runtime Exception)和检查异常(Checked Exception)两种,如图8-1所示。为了深入了解执行异常和检查异常内容,这里给出它们的详细介绍列举。

8-1  异常类的继承结构

1.执行异常

执行异常即运行时异常,继承于Runtime ExceptionJava编译器允许程序不对它们做出处理。下面列出了主要的运行时异常。

·    ArithmeticException一个非法算术运算产生的异常。

·    ArrayStoreException存入数组的内容数据类型不一致所产生的异常。

·    ArrayIndexOutOfBoundsException数组索引超出范围所产生的异常。

·    ClassCastException类对象强迫转换造成不当类对象所产生的异常。

·    IllegalArgumentException程序调用时,返回错误自变量的数据类型。

·    IllegalThreadStateException线程在不合理状态下运行所产生的异常。

·    NumberFormatException字符串转换为数值所产生的异常。

·    IllegalMonitorStateException线程等候或通知对象时所产生的异常。

·    IndexOutOfBoundsException索引超出范围所产生的异常。

·    NegativeException数组建立负值索引所产生的异常。

·    NullPointerException对象引用参考值为null所产生的异常。

·    SecurityException违反安全所产生的异常。

2.检查异常

除了执行异常外,其余的子类是属于检查异常类也称为非运行时异常,它们都在java.lang类库内定义。Java编译器要求程序必须捕获或者声明抛弃这种异常。下面列出了主要的检查异常。

·    ClassNotFoundException找不到类或接口所产生的异常。

·    CloneNotSupportedException使用对象的clone( )方法但无法执行Cloneable所产生的异常。

·    IllegalAccessException类定义不明确所产生的异常。

·    InstantiationException使用newInstance( )方法试图建立一个类instance时所产生的异常。

·    InterruptedException目前线程等待执行,另一线程中断目前线程所产生的异常。

8.3.2  错误分类

Error类与异常一样,它们都是继承自java.lang.Throwable类。Error类对象由Java虚拟机生成并抛出。Error类包括LinkageError(结合错误)与VitualmanchineError(虚拟机错误)两种子类。

1LinkageError

LinkageError类的子类表示一个类信赖于另一个类,但是,在前一个类编译之后,后一个类的改变会与它不兼容。

LinkageError类包括ClassFormatErrorClassCircularityErrorExceptionInitializerErrorNoClassDeFormatErrorVeritfyErrorUnsatisfidLinkError IncompatibleClassChangeError等子类。其中NoIncompatibleClassChangeError类又包含AbstractMethodErrorNoSuchField ErrorNoSuchMethodErrorIllegalAccessErrorInstantiationError子类。这些类所代表的意义如下所述。

·    ClassFormatError类格式所产生的错误。

·    ClassCircularityError无限循环所产生的错误。

·    ExceptionInitializerError初始化所产生的错误。

·    NoClassDeFormatError没有类定义所产生的错误。

·    VeritfyError类文件某些数据不一致或安全问题所产生的错误。

·    UnsatisfidLinkErrorJava虚拟机无法找到合适的原始语言(Native-Language)定义的方法所产生的错误。

·    IncompatibleClassChangeError不兼容类所产生的错误。

·    AbtractMethodError调用抽象方法所产生的错误。

·    NoSuchFieldError存取或改变数据域所产生的错误。

·    NoSuchMethodError调用类方法所产生的错误。

·    IllegalAccessError不合法存取或改变数据域调用方法所产生的错误。

·    InstantiationError使用抽象类或接口所产生的错误。

2VitualmachineError

Java虚拟机崩溃了或用尽了它继续操作所需的资源时,抛出该错误。

VitualmachineError包含InternalErrorOutOfMemoryErrorStackOverflow ErrorUnknownError。这些类所代表的意义如下所述。

·    InternalError虚拟机内部所产生的错误。

·    OutOfMemoryError虚拟机内存不足所产生的错误。

·    StackOverflowError堆栈无法容纳所产生的错误。

·    UnknownError虚拟机不知名异常所产生的错误。

8.3.3  异常处理机制

Java提供了一种独特的异常处理机制,通常通过异常来处理程序设计中可能出现的错误

Java程序的执行过程中,如果出现了异常事件,就会生成一个异常对象;生成的异常对象将传递给Java运行系统,这一异常的产生和提交过程称为抛弃(Throw)异常。当Java运行系统得到一个异常对象时,它将会寻找处理这一异常的代码,找到能够处理这种类型异常的方法后,运行系统把当前异常对象交给这个方法进行处理,这一过程称为捕获(Catch)异常。如果Java运行系统找不到可以捕获异常的方法,则运行系统将终止,相应的Java程序也将退出。

Java异常处理是通过5个关键字来管理的。它们是trycatchthrowthrowsfinally,将在下面的小节中详细介绍。这里先大致给出它们的工作原理。

程序中需要被监测的程序语句序列应包含在一个try代码块中。如果try代码块中有异常发生,那么就要抛出该异常。可以用catch代码块来捕获这个异常,并且在catch代码块中加以适当的处理。系统产生的异常会由Java运行系统自动抛出。如果需要手动抛出异常,则使用关键字throw。在某些情况下,从一个方法抛出的异常必须用一个throw语句指定为异常。最后,从try代码块退出时,必须执行的代码要放在一个finallly代码块中。

异常机制提供了程序退出的安全通道。当出现错误后,程序执行的流程发生改变,程序的控制权转移到异常处理器。传统的处理异常的办法是:方法返回一个特殊的结果来表示出现异常,调用该方法的程序负责检查并分析函数返回的结果并进行相应的处理。但是,这样做有如下弊端:

·    函数返回-1代表出现异常,但是如果函数确实要返回-1这个正确的值时,就会出现混淆。

·    可读性降低,将程序代码与处理异常的代码交叉在一起。

·    由调用函数的程序来分析错误,这就要求客户程序员对库函数有很深的了解。

Java的异常可以分为运行时异常和非运行时异常两类。继承于RuntimeException的类都属于运行时异常,例如算术异常、数组下标越界异常等。由于这些异常产生的位置是未知的,Java编译器允许程序员在程序中不对它们做出处理。除了运行时异常之外的其他由Exception继承来的异常都是非运行时异常,Java编译器要求在程序中必须处理这种异常

8.3.4  异常处理语句

异常处理的目的并不是为了避免发生异常,而是在异常发生时避免程序的异常终止,设法将损失降低到最小。

Java的异常处理是通过5个关键词来实现的:trycatchthrowthrowsfinally。一般情况下是用try来执行一段程序,如果出现异常,系统会抛出(throw)一个异常,这时候可以通过它的类型来捕捉(catch)它,最后(finally)由默认处理器来处理。

1try/catch语句块

Java程序里,异常对象是依靠try/catch语句来捕获和处理的。try/catch异常处理语句分为try语句块和catch语句块,其格式如下。

try{

    ……//try语句块,可能产生异常的多个语句

}catch{

    ……//catch语句块,对异常进行处理

}

一般将可能产生异常情况语句放在try语句块中,这个try语句块用来启动Java的异常处理机制。凡是可能抛出异常的语句,包括throw语句和可能抛出异常的方法的调用语句,都应该包含在这个try语句块中。然后在catch语句块对异常进行处理。Java语言还规定,每个catch语句块都应该与一个try语句块相对应。

【例8-1捕获除数为零的异常,并显示相应信息。

class ArithmeticExceptionDemo{

    public static void main(String args[]) {

        int zero,aInt;

        try {// 监视可能产生异常的代码块

            zero=0;

            aInt=68/zero;

            System.out.println("本字符串将不显示。");

        }catch (ArithmeticException e) {       //捕获divide-by-zero错误

            System.out.println("产生用零除错误。");

        }

        System.out.println("在捕获语句后执行的一个语句。");

    }

}

该程序的执行结果如图8-2所示。

8-2  捕获divide-by-zero错误的执行结果

Try语句块中调用了可能抛出ArithmeticException的对象,catch语句块则专门用来捕获这类异常。可见,catch语句块应紧跟在try语句块的后面。当try语句块中的某条语句在执行时产生了一个异常,此时被启动的异常处理机制会自动捕获到它,然后流程自动跳过异常引发点后面的所有尚未执行语句,而转至try语句块后面的catch语句块,执行catch语句块中的语句。从执行的结果还可以看出,异常被捕获并执行完catch语句块中的语句后,继续执行catch语句块之后的语句。如果没有异常发生,则跳过catch语句块。

2finally语句块

finally语句块用来控制从try-catch语句转移到另一部分之前的一些必要的善后工作,这些工作包括关闭文件或释放其他有关系统资源等。finally语句块中的语句是一种强制的、无条件执行的语句,即无论在程序中是否出现异常,无论出现哪一种异常,也不管try代码块中是否包含有breakcontinuereturn或者throw语句,都必须执行finally语句块中所包含的语句。

finally语句块紧接着try-catch结构中的最后一个catch语句块,其形式如下:

try{

        ……

}catch(Exception1 e1){

        ……

}catch(Exception2 e2){

        ……

} finally{

        ……

}

在出现和未出现异常的情况下都要执行的代码可以放到finally语句块中。加入了finally语句块后有以下3种执行情况。

·    没有抛出异常情况:执行try语句块后,再执行finally语句块。

·    代码抛出在catch语句块中捕获的一个异常情况:这时,Java执行try语句块中直到这个异常情况被抛出为止的所有代码,跳过try语句块中剩余的代码;然后执行匹配的catch语句块中的代码和finally语句块中的代码。

·    代码抛出了一个在catch语句块中没有捕获到的异常情况:这时,Java执行try语句块中直到这个异常情况被抛出为止的所有代码,跳过try语句块中剩余的代码;然后执行finally语句块中的代码,并把这个异常情况抛回到这个方法的调用者。

【例8-2带有finally语句块的程序示例。

class Finally_Demo{

    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块内");

            }

        }

    }

}

该程序的执行结果如图8-3所示。

8-3  带有finally语句块的程序的执行结果

Java语言中,try-catch-finally语句允许嵌套。本例就是将内层的一个try嵌套在外层的finally语句块内。在程序执行到外层的try语句块时,由于分母为零而产生了算术异常,所以程序转移到第一个catch语句块。该catch语句块捕获了这个算术异常,并进行了处理,之后再转向必须执行的外层的finally语句块。因为在该finally语句块内又产生了空指针异常(一个null字符串和字符串"王老三"进行比较),所以内层catch语句块又捕获到NullPointerException,最后程序转移到内层的finally语句块。

finally语句块还可以和breakcontinue以及return等流程控制语句一起使用。当try语句块中出现了上述语句时,程序必须先执行finally语句块,才能最终离开try语句块。

【例8-3同时有break语句和finally语句块的程序的执行情况。

class FinallyWithBreakDemo{

    public static void main(String args[]) {

        for( ; ; )

        try{

            System.out.println("即将被break中断,要退出循环了!");

            break;

        }finally{

            System.out.println("但是finally块总要被执行到!");

        }

    }

}

该程序的执行结果如图8-4所示。

8-4  同时有finally语句块和break语句的程序的执行结果

8.3.5  声明异常

在某些情况下,如果一个方法产生自己不处理或无法处理的异常,它就必须在throws子句中声明该异常。也就是说,在Java语言中如果在一个方法中生成了一个异常,但是这一方法并不确切地知道该如何对这一异常事件进行处理,这时,这个方法就应该声明抛弃异常,使得异常对象可以从调用栈向后传播,直到有适合的方法捕获它为止。

throws关键字是在方法声明中用来列出从方法中发出的非起源于ErrorRutimeException中的任何异常。能够主动引发异常的方法必须用throws来声明。通常使用Java预定义的异常类就可以满足程序开发者的编程需要。

声明抛弃异常是在一个方法中的throws子句中指明的。

下面是包含throws子句的方法的基本形式。

[修饰符]  返回类型  方法名(参数1,参数2,……)throws异常列表

   {……}

例如:

public int read ( ) throws IOException

   {……}

throws子句中同时可以指明多个异常,说明该方法将不对这些异常进行处理,而是声明抛弃它们。例如:

public static void main (String args[ ]) throws IOException, IndexOutOf-
BoudsException

   {……}

【例8-4声明抛出异常的程序格式。

import java.io.*;

public class ExceptionExam5

{

public static void go() throws IOException

    {//方法代码}

public static void main(String [] args)

{//程序入口主方法代码}

}

因为考虑到go()方法可能产生一个IOException,而此时无法处理异常,所以要从go()方法抛出这个异常,并且需要用throws子句指定异常。另外,JavaI/O系统包含在java.io包中,因此IOException也包含在其中,所以使用语句“import java.io.*;”导入java.io包,然后可以直接引用IOException

8.3.6  抛出异常

Java应用程序在运行时如果出现了一个可识别的错误,就会产生一个与该错误相对应的异常类的对象。这个对象包含了异常的类型和错误出现时程序所处的状态信息,该异常对象首先被交给Java虚拟机,由虚拟机来寻找具体的异常处理者。在Java中把产生异常对象并将其交给Java虚拟机的过程称为抛出异常。

异常类不同,抛出异常的方法也不同,可以分为以下两种。

·    系统自动抛出的异常。

·    语句抛出的异常。

系统定义的所有运行异常都可以由系统自动抛出。例如,以非法的算术操作引发的算术异常,这时系统抛出已定义好的异常类ArithmeticException的对象。前面列出的例子中,基本都属于系统自动抛出的异常。

语句抛出的异常是借助throw语句定义何种情况产生这种异常。用户程序自定义的异常不可能依靠系统自动抛出,必须使用throw语句抛出这个异常类的新对象。系统定义的运行异常也可以由throw语句抛出。用throw语句抛出异常对象的一般步骤如下:

1)指定或定义一个合适的异常情况类。

2)产生这个类的一个对象。

3)抛出它。

例如:

EOFException e = new EOFException( );

throw e;

使用throw语句抛出异常有两种方式:直接抛出和间接抛出。

1.直接抛出异常

直接抛出方式是直接利用throw语句将异常抛出,格式为:

throw newExceptionObject;

利用throw语句抛出一个异常后,程序执行流程将直接寻找一个捕获(catch)语句,并进行匹配执行相应的异常处理程序,其后的所有语句都将被忽略。

【例8-5设计自己的异常类,从键盘输入一个double类型的数,若不小于0.0,则输出它的平方根;若小于0.0,则输出提示信息“输入错误”。

import java.io.*;

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{

            System.out.print("求输入实数的平方根。请输入一个实数:");

            BufferedReader br=

                new BufferedReader(new InputStreamReader(System.in) );

            String s=br.readLine();

            n.test(Double.parseDouble(s));

        }catch(MyException e){

            System.out.println("输入错误!");

        }

    }

}

程序的两次运行结果如图8-5所示。

8-5  MyException类的两次运行结果

在这个程序中,定义的异常类通过extends子句继承了Exception异常类。在test( )方法中,用throw语句指定了可能抛出的异常,该语句在参数小于0时被执行,产生并抛出异常

值得注意的是:在一个方法定义中如果采用了throw语句直接抛出异常,则该方法在发生异常的情况下可能没有返回值。本例就属于这种情况。

从本例也可以看出:由于系统不能识别用户自定义的异常,所以需要编程人员在程序中的合适位置创建自定义异常的对象,并利用throw语句将这个新异常对象抛出。

2.间接抛出异常

Java程序中,可以在方法的定义中利用throws关键字声明异常类型而间接抛出异常。也就是说,当Java程序中方法本身对其中出现的异常并不关心或不方便处理时,可以不在方法实现中直接捕获有关异常并进行处理,而是在方法定义的时候通过throws关键字,将异常抛给上层调用处理。其形式如下。

public void myMethod1() throws IndexOutOfBoundsException {

    ……

}

public void myMethod2() throws myException1, myException2 {

    ……

}

在上层调用该方法时,必须捕获有关异常,否则编译时将会出错。例如,调用方法myMethod2()时,必须按如下方式进行。

try{

    myMethod2

}catch (MyExceptionl e1){

    ……

}catch(MyException2 e2){

    ……

}

【例8-6带有间接抛出异常的类。

public class OutOfRangeException extends Exception{

        public OutOfRangeException(){};

        public OutOfRangeException(Sting s){

            super(s);

        }

} // 定义一个异常类

import OutOfRangeException;         //装载异常类

import java.io.*;

public class CreatingExceptions{

    private static BufferedReader in = new BufferedReader

        (new InputStreamReader(System.in));

    public static void main (String[] args) throws OutOfRangeException{

        final int MIN = 25, MAX = 40;

        int value;

        OutOfRangeException problem =

            new OutOfRangeException ("Input value is out of range.");

            //创建一个异常对象并可能抛出它

        System.out.print ("Enter an integer value between " + MIN +

                              " and " + MAX + ", inclusive: ");

        try{

            value = Integer.parseInt (in.readLine());

        }catch (Exception exception) {

            System.out.println ("Error reading int data, MIN_VALUE value

                                    returned.");

            value = Integer.MIN_VALUE;

         }

            //确定该异常是否抛出

        if (value < MIN || value > MAX)

            throw problem;

        System.out.println ("End of main method.");

            //may never reach this place

    }

}

这个例子有两个特征,一是它利用了自定义的异常类OutOfRangeException,一旦程序执行违背了所定义的逻辑就抛出这个异常;二是在抛出异常的方法中,利用throws关键字声明了OutOfRangeException异常类的间接抛出,这样若是在其他地方使用到这个类的对象,也可以捕获这个异常。

使用throws子句抛出异常时应注意如下两个问题。

·    一般这种抛出异常的语句应该被定义为在满足一定条件时执行,例如把throws子句放在if语句的条件分支中,只有当条件得到满足,即用户定义的逻辑错误发生时才抛出。例如,例8-6中的条件(value < MIN || value > MAX)满足时,抛出异常。

·    对于含有throws子句的方法,应该在方法头定义中增加如下部分。

throws异常类名列表

这样做主要是为了通知所有欲调用此方法的方法:由于该方法包含throws了句,所以要准备接受和处理它在运行过程中可能会抛出的异常。如果方法中的throws了句不止一个,方法头的异常类名表中的列出的异常也不止一个,应该包含所有可能产生的异常。例如,在上面的myMethod2( )方法中包含的异常有:myException1myException2

注意:执行throws子语句将中断程序的执行,也就是说throws的下一条语句将暂停执行。

8.3.7  自定义异常类

在实际的编程中并不一定非要使用Java已经定义的异常,经常需要创建自己的异常,以便指出编写的代码可能生成的一个特殊错误。创建自己的异常类,必须从一个现有的异常类型(最终继承自Throwable类)继承。继承一个异常同继承一个普通的类的方法是一样的

Java提供的一些异常有时候不能满足编程的需求,例如规定用户输入数据的范围在2030之间,但是Java并没有这个方面的异常,这时就可以应用自定义的异常来规范化客户的数据输入。

Java中进行自定义异常时,自定义异常类必须是Throwable类的直接或间接子类。下面的例子是关于自定义异常的。它通过继承Exception类而继承Throwable类,即间接继承Throwable类。

【例8-7自定义异常类程序示例。

class OutBoundsException extends Exception

{

    OutBoundsException (String mes)

    {

        //调用超类的构造函数

        super(mes);

    }

}

class check

{

    String ChecktheNum(int n) throws OutBoundsException

    {

        Integer N=new Integer(n);

        if(n>30||n<20)

    throw  new OutBoundsException("the number is out of bound!!");

        else

        return "the number"+N.toString()+"is in the bound!!";

    }

}

class Test

{

    public static void main(String []args)

    {

        try

        {

            check c=new check();

            System.out.println("以下是合法的数据的报告!");

            System.out.println(c.ChecktheNum(25));

            System.out.println("以下是非法的数据的报告!");

            System.out.println(c.ChecktheNum(5));

        }

        catch(OutBoundsException e)

        {

            System.out.println(e.toString());

        }

    }

}

运行结果如图8-6所示。

8-6  运行结果

注意:一个方法所声明抛弃的异常是作为这个方法与外界交互的一部分而存在的。所以,方法的调用者必须了解这些异常,并确定如何正确地处理它们。