Java学习总结——异常的捕获与处理

拾玖——异常的捕获与处理

不管使用的是哪种语言进行程序设计,都会产生各种各样的错误。Java 提供有强大的异常处理机制。在 Java 中,所有的异常被封装到一个类中,程序出错时会将异常抛出。

一个程序能在正常情况正确地运行,这是一个程序的基本要求。但一个健壮的程序则需要考虑很多使程序失效的因素,即它要在非正常的情况下,也要能进行必要的处理。程序是由程序员编写的,而程序员是存在思维盲点的,一个合格的程序员能保证 Java 程序不会出现编译错误,但却无法 “ 考虑完备 ”,确保程序在运行时一定不会发生错误,而这些运行的错误,对 Java 而言是一种异常。有了异常,就应有相应的手段来处理这些异常,这样才能确保这些异常不会导致丢失数据或破坏系统运行等灾难性后果。

一、异常的基本概念

Java 的程序代码在一些特定环境下,在会发生某些不测情况。在编程过程中,首先应当尽可能去避免错误和异常发生,对于不可避免、不可预测的情况则要考虑异常发生时如何处理。

异常( Exception )也称为例外,指的是所有可能造成计算机无法正常处理的情况,如果没有实现妥善的安排,严重的话可以使得计算机死机。异常处理是一种特定的程序错误处理机制,是为了让程序员更加关注正常的程序执行序列而设计的。异常处理提供了一种标准的方法以处理错误,发现可预知及不可预知的问题,及允许开发者识别、查出和修改错漏之处。

处理错误的方法有如下几个特点:

(1)不需要打乱程序的结构,如果没有任何错误产生,那么程序的运行不受任何影响。

(2)不依靠方法的返回值来报告错误是否产生。

(3)采用集中的方式处理错误,能够根据错误种类的不同来进行对应的错误处理操作。

下面列出 Java 中几个常见的异常,括号内所注的英文是对应的异常处理类名称:

(1)算术异常( ArithmeticException ):当算数运算中出现了除以零这样的运算就会出现这样的异常。

(2)空指针异常( NullPointerException ):没有给对象开辟内存空间却使用该对象时会出现空指针异常。

(3)文件未找到异常( FileNotFoundException ):当程序试图打开一个不存在的文件进行读写时将会引发该异常。经常是由于文件名给错,或者要存储的磁盘、CD-ROM 等被移走,没有放入等原因造成。

(4)数组下标越界异常( ArrayIndexOutOfBoundsException ):对于一个给定的大小的数组,如果数组的索引超过上限或低于下限都造成越界。

(5)内存不足异常( OutOfMemoryException ):当可用内存不足以让 Java 虚拟机分配给一个对象时抛出该错误。

Java 的异常处理机制也秉承着面向对象的基本思想。在 Java 中,所有的异常都是以类的类型存在。除了内置的异常类之外,Java 也可以自定义异常类。此外,Java 的异常处理机制也允许自定义抛出异常。

1.为何要处理异常

异常是在程序运行过程中发生的事件,例如除以零溢出、数组越界、文件找不到等,这些事件的发生将组织程序的正常运行。为了加强程序的健壮性( Robust ),在程序设计时,必须考虑到可能发生的异常时间,并作出相应的处理。在 C 语言中,可通过使用 if…else… 语句控制异常。同时,可通过被调用函数的返回值感知在其中产生的异常事件,并进行处理。然而,使用函数返回的全局 ErroNo( 错误代码 ),仅能用来反映一个异常事件的类型。这种异常监控模式非常繁琐,同一个异常或者错误如果多个地方出现,那么在每个地方都要做相同处理。

在程序贬值过程中,有一个 80/20 原则,即 80% 的精力花费在 20% 的事情上,而这 20% 的事情就是处理各个可能出现的错误或异常。如果想编制一个完善的高容错运行程序,且没有使用异常处理机制的话,程序中将充斥着 if 语句,整个程序的结构就会变得臃肿而混乱。而事实上,由于程序员本身存在思维盲点,即使再简单的程序,要把其所有可能出现错误都预想到也是不现实的。

Java 通过面向对象的方法来处理异常。在一个方法的运行过程中,如果发生了异常,则这个方法生成代表该异常的一个对象,并把它交给运行时系统,运行时系统寻找相应的代码来处理这一异常。我们把生成异常对象并把它提交给运行时系统的过程称之为异常的抛出( throw )。运行时系统在方法的调用栈中查找,从生成异常的方法开始进行回溯,知道找到包含相应异常处理的方法为止,这一过程称之为异常的捕获( catch )。

2.简单的异常范例

Java 本身已有较完善的机制来处理异常的发生。

下面的 TestException 是一个错误的程序,它在访问数组时,下标值已超过了数组下标所允许的最大值,因此会有异常发生。

举例:

//数组越界异常
public class TestException
{
	public static void main(String[] args)
	{
		int[] arr = new int[5];	//允许5个元素
		arr[10] = 7;	//下标值超出所允许的范围
	}
}

异常产生的原因在于,数组的下标值超出了最大允许的范围。Java 检测这个异常之后,便由系统抛出 “ ArrayIndexOutOfBoundsException ”,用来表示错误的原因,并停止运行程序。如果没有编写相应的处理异常的程序代码,Java 的默认异常处理机制会先抛出异常,然后停止运行程序。

在出现异常之后,异常语句后的代码将不再执行,而是直接结束了程序的运行,那么就表示此时程序是处于一种 “ 不健康 ” 的状态。为了保证程序出现异常之后,依然可以完整运行,就需要引入异常处理操作。

3.异常的处理

在上例代码的异常发生后,Java 便把这个异常抛了出来,可是抛出来之后没有代码去捕捉它。如果加上捕捉异常的程序代码,则可针对不同的异常做妥善的处理,这种处理的方式称为异常处理。

异常处理是由 try、catch 与 finally 等 3 个关键字所组成的程序块,其语法如下所示( 方括号内的部分是可选部分 )。

		try{
		    要检查的程序语句;
		    ……
		}
		catch(异常类 对象名称){
		    异常发生时的处理语句;
		}
		[
		catch(异常类 对象名称){
		    异常发生时的处理语句;
		}
		catch(异常类 对象名称){
		    异常发生时的处理语句;
		}
		……
		]
		[
		finally{
		    一定会运行到的代码;
		}
		]

Java 提供了 try( 尝试 )、catch( 捕捉 )及 finally( 最终 )这 3 个关键词来处理异常。这 3 个动作描述了异常处理的 3 个流程。

(1)首先,我们把所有可能发生异常的语句都放到一个 try 之后由 { } 所形成的区块称为 “ try 区块 ”( try block )。程序通过 try{} 区块准备捕捉异常。try 程序块若有异常发生,程序的运行便中断,并抛出 “ 异常类所产生的对象 ”。

(2)抛出的对象如果属于 catch() 括号内欲捕获的异常类,catch 则会捕捉此异常,然后进入 catch 的块里继续运行。

(3)无论 try 程序块是否捕捉到异常,或者捕捉到的异常是否与 catch() 括号里的异常相同,最终一定会运行 finally 块里的程序代码。

finally 的程序代码块运行结束后,程序再回到 try-catch-finally 块之后继续运行。

综上,在异常捕捉的过程中至少做了两个判断:第一个是 try 程序块是否有异常产生,第 2 个是产生的异常是否和 catch() 括号内欲捕捉的异常相同。

值得一提的是,finally 块是可以省略的。如果省略了 finally 块,那么在 catch() 块运行结束后,程序将跳到 try-catch 块之后继续运行。

异常的处理格式之中可以分为三类:try{ }…catch{ }、try{ }…catch{ }…finally{ }、try{ }…finally{ }。

在处理各种异常,需要用到对应的 “ 异常类 ”,“ 异常类 ” 指的是由程序抛出的对象所属的类。

举例:

//异常处理的使用
public class DealException
{
	public static void main(String[] args)
	{
		try {
			//检查这个程序块的代码
			int[] arr = new int[5];	//允许5个元素
			arr[10] = 7;	//下标值超出所允许的范围,在这里会出现异常
		} catch (ArrayIndexOutOfBoundsException ex) {
			// TODO: handle exception
			System.out.println("数组下标越界!");
			System.out.println("异常:" + ex);    //显示异常对象ex的内容
		}
		finally{
			//这个块的程序代码一定会执行
			System.out.println("这里一定会执行!");
		}
		System.out.println("main()方法结束!");
	}
}

通过异常处理机制,即使程序运行时发生问题,只要能捕捉到异常,程序便能顺利地运行到最后,而且还能适时的加入到错误信息的提示。

如果程序捕捉到了异常,则在 catch 括号内的异常类 ArrayIndexOutOfBoundsException 之后生成一个对象 ex,利用此对象可以得到异常的相关信息。

如果想要得到详细的异常信息,则需要使用异常对象的 printStackTrace() 方法,如下:

        ex.printStackTrace();

此方法会给出更为详细的异常信息,不仅包括异常的类型,还包括异常发生在哪个所属包、哪个所属类、哪个所属方法以及发生异常的行号。

4.异常处理机制小结

当异常发生时,通常可用两种方法来处理:

一种是交由 Java 默认的异常处理机制做处理。但这种处理方式,Java 通常异能输出异常信息,接着便终止程序的运行,结束类的运行。

另一种处理方式是用自行编写的 try-catch-finally 块来捕捉异常。自行编写程序代码来捕捉异常最大的好处是,可以灵活操控程序的流程,且可做出最适当的处理。

二、异常类的处理流程

异常可分为两大类:java.lang.Exception 类与 java.lang.Error 类。这两个类均继承自 java.lang.Throwable 类。

习惯上将 Error 类与 Exception 类统称为异常类,但两者本质上还是不同的。Error 类通常指的是 Java 虚拟机( JVM )出错,用户无法在程序里处理这种错误。

不同于 Error 类,Exception 类包含了一般性的异常,这些异常通常在捕捉到之后便可做妥善的处理,以确保程序继续运行。在日后进行异常处理的操作之后,默认是针对 Exception 进行处理,而对于 Error 而言,无需普通用户关注。为了更好地说明 Java 之中异常处理的操作特点,下面给出异常出的流程。

(1)如果程序之中产生了异常,那么会自动地由 JVM 根据异常的类型,实例化一个指定异常类的对象;如果这个时候程序之中没有任何的异常处理操作,则这个异常类的实例化对象将交给 JVM 进行处理,而 JVM 的默认处理方式就是进行异常信息的输出,而后中断程序执行。

(2)如果程序之中存在了异常处理,则会由 try 语句捕获产生的异常类对象;然后将该对象与 try 之后的 catch 进行匹配,如果匹配成功,则使用指定的 catch 进行处理,如果没有匹配成功,则向后面的 catch 继续匹配,如果没有任何的 catch 匹配成功,则这个时候交给 JVM 执行默认处理。

(3)不管是否有异常都会执行 finally 程序,如果此时没有异常,执行完 finally,则会继续执行程序之中的其他代码,如果此时有异常没有能够处理( 没有一个 catch 可以满足 ),那么也会执行 finally,但是执行完 finally 之后,将默认交给 JVM 进行异常的信息输出,并且程序中断。

三、throws 关键字

在 Java 标准库中的方法通常并没有处理异常,而是交由使用者来处理,如判断整数数据格式是否合法的 Integer.parseInt() 方法就会抛出 NumberFormatException 异常。这是怎么做到的?我们看一下 API 文档中的方法原型。

        public static int parseInt(String s)
        throws NumberFormatException

就是这个 “ throws ” 关键字,如果字符串 s 中没有包含可解析的整数就会 “ 抛出 ” 异常。使用 throws 声明的方法表示此方法不处理异常,而由系统自动将所捕获的异常信息 “ 抛给 ” 上级调用方法。

throws 使用格式如下:

        访问权限 返回值类型 方法名称(参数列表) throws 异常类
        {
            //方法体;
        }

上面的格式包括两个部分:一个普通方法的定义,方法后紧跟 “ throws 异常类 ”,它位于方法体 { } 之前,用来检测当前方法是否有异常,若有,则将该异常提交给直接使用这个方法的方法。

四、throw 关键字

所有的异常类对象全部都是由 JVM 自动实例化的,但有时用户希望能亲自进行异常类对象的实例化操作,自己手工抛出异常,那么此时就需要依靠 throw 关键字来完成。

与 throws 不同的是,可直接使用 throw 抛出一个异常,抛出时直接抛出异常类的实例化对象即可。

throw 语句的格式为:

        throw 异常类型的实例;

执行这条语句时,将会 “ 引发 ” 一个指定类型的异常,也就是抛出异常。

举例:

//关键字throw的使用
public class throwDemo
{
	public static void main(String[] args)
	{
		try {
			//抛出异常的实例化对象
			throw new ArrayIndexOutOfBoundsException("数组下标越界");
		} catch (ArrayIndexOutOfBoundsException ex) {
			// TODO: handle exception
			System.out.println(ex);
		}
	}
}

throw 关键字的使用完全符合异常的处理机制,但是,一般来讲用户都在避免异常的产生,所以不会手工抛出一个新的异常类型的实例,而往往会抛出程序中已经产生的异常类实例。

五、异常处理的标准格式

实际上,try…catch…finally、throw 及 throws 经常联合使用。例如,现在要设计一个将数组指定下标的元素置零的方法,同时要求在方法的开始和结束处都要输出相应信息。

代码如下:

//关键字throws与throw的配合使用
public class throwDemo
{
	public static void main(String[] args)
	{
		int[] arr = new int[5];
		try{
			setZero(arr,10);
		}
		catch(ArrayIndexOutOfBoundsException e){
			System.out.println("异常:" + e); 	//显示异常对象e的内容
		}
		System.out.println("main()方法结束!");
	}
	
	private static void setZero(int[] arr,int index)
		throws ArrayIndexOutOfBoundsException
	{
		System.out.println("————————方法setZero开始————————");
		try {
			arr[index] = 0;
		} catch (ArrayIndexOutOfBoundsException ex) {
			// TODO: handle exception
			throw ex;
		}
		finally{
			System.out.println("————————方法setZero结束————————");
		}
	}
}

六、RuntimeException 类

在 Java 面试中经常会询问 Exception 类与 RuntimeException 类的区别,如果想理解这两个类的区别,请看如下代码:

public class RuntimeExceptionDemo
{
	public static void main(String[] args)
	{
		String str = "123";
		int temp = Integer.parseInt(str);	//将字符串变为int
		System.out.println(temp*2);
	}
}

代码中没有任何有关异常件检测的语句,现在来观察一下 Integer 类之中的 parseInt() 方法定义。

        public static int parseInt(String s) throws NumberFormatException;

通过查看 parseInt() 方法的原型,就可以发现,parseInt() 使用了 throws 抛出的一个异常。在 API 中查一下 NumberFormatException 类的继承体系。

		java.lang.Object
			|- java.lang.Throwable
				|- java.lang.Exception
					|- java.lang.RuntimeException
						|- java.lang.IllegalArgumentException
							|- java.lang.NumberFormatException

可以发现 NumberFormatException 类继承自 RuntimeException 类,而在 Java 中明确规定对于 RuntimeException 的异常类型可以有选择性地来进行处理,如果不处理则出现异常时将交给 JVM 默认处理。

常见的 RuntimeException 类型的异常有:NumberFormatException、ClassCastException、NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException。

Exception 和 RuntimeException 的区别如下:

Exception :强制性要求用户必须处理。

RuntimeException :是 Exception 的子类,由用户选择是否进行处理。

七、编写自己的异常类

在 Java 中本身已经提供了大量的异常类型,但是在开发之中,这些异常类型可能会不能满足于全部的开发需求,特别是在做一些软件的架构设计的时候,这些异常类可能不够用户去使用。为了处理各种异常,Java 可通过继承的方式运行用户编写自己的异常类。因为所有可处理的异常类均继承自 Exception 类,因此自定义异常类也不例外。

自定义编写异常类的语法如下:

		class 异常名称 extends Exception
		{
		    ……
		}

我们可以在自定义异常类里编写方法来处理相关的事件,甚至不便携任何语句也可以正常地工作,这是因为父类 Exception 已提供相当丰富的办法,通过继承子类均可使用它们。

下面通过一例代码来说明如何定义自己的异常类,以及如何使用它们。

代码如下:

//定义自己的异常类
public class userDefinedException
{
	public static void main(String[] args)
	{
		try {
			throw new MyException("自定义异常");
		} catch (MyException e) {
			// TODO: handle exception
			System.out.println(e);
		}
	}
}

class MyException extends Exception
{
	public MyException(String msg)
	{
		super(msg);	//调用Exception类的构造方法,存入异常信息
	}
}

Exception 构造方法如下:

        public Exception(String message)

在 JDK 中提供的大量 API 方法之中含有大量的异常类,但这些类在实际开发中往往并不能完全满足设计者对程序异常处理的需要,在这个时候就需要用户自己去定义所需的异常类,用一个类清楚地写出所需要处理的异常。

八、本文注意事项

1.异常类型的继承关系

异常类型的最大父类时 Throwable 类,其分为两个子类,分别为 Exception、Error。

Exception 表示程序可处理的异常,而 Error 表示 JVM 错误,一般无需程序开发人员自己处理。

2.RuntimeException 和 Exception 的区别

RuntimeException 类是 Exception 类的子类,Exception 定义了必须处理的异常,而 RuntimeException 定义的异常可以选择性地进行处理。

你可能感兴趣的:(Java)