一、 异常的定义
异常指不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。
在JAVA语言出现以前,传统的异常处理方式多采用返回值来标识程序出现的异常情况,这种方式虽然为程序员所熟悉,但却有多个坏处。
首先,一个API可以返回任意的返回值,而这些返回值本身并不能解释该返回值是否代表一个异常情况发生了和该异常的具体情况,需要调用API的程序自己判断并解释返回值的含义。
其次,并没有一种机制来保证异常情况一定会得到处理,调用程序可以简单的忽略该返回值,需要调用API的程序员记住去检测返回值并处理异常情况。这种方式还让程序代码变得晦涩冗长,当进行IO操作等容易出现异常情况的处理时,你会发现代码的很大部分用于处理异常情况的switch分支,程序代码的可读性变得很差。
上面提到的问题,JAVA的异常处理机制提供了很好的解决方案。通过抛出JDK预定义或者自定义的异常,能够表明程序中出现了什么样的异常情况;而且JAVA的语言机制保证了异常一定会得到恰当的处理;合理的使用异常处理机制,会让程序代码清晰易懂。
Java通过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例,描述了出现在一段编码中的错误条件。当条件生成时,错误将引发异常。
Java异常类层次结构图:
图1 Java异常类层次结构图
在 Java中,所有的异常都有一个共同的祖先
Throwable(可抛出)。Throwable指定代码中可用异常传播机制通过 Java应用程序传输的任何问题的共性。
Throwable:有两个重要的子类:Exception(异常)和
Error(错误),二者都是 Java异常处理的重要子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual Machine Error),当 JVM不再有继续执行操作所需的内存资源时,将出现 Out Of Memory Error。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual Machine Error)、类定义错误(No Class DefFound Error)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception(异常):是程序本身可以处理的异常。
Exception 类有一个重要的子类 RuntimeException。RuntimeException类及其子类表示“JVM常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。
注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
通常,Java的异常(包括Exception和Error)分为可查的异常(checked
exceptions)和不可查的异常(unchecked exceptions)。
可查异常(编译器要求必须处置的异常):正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。
除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
不可查异常(编译器不要求强制处置的异常):包括运行时异常(RuntimeException与其子类)和错误(Error)。
Exception这种异常分两大类运行时异常和非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。
运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
非运行时异常(编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
二、异常类
由继承类图可以看出,Throwable类派生了两个类:Exception类和Error类,其中Error类系统保留,而Exception类供应用程序使用,它下面又派生出几个具体的异常类,都对应着一项具体的运行错误,如图2所示
图2 异常类图
Exception类中常用的方法为:
toString()函数
返回异常的简短描述。
getMessage()函数
返回异常的详细信息描述。
printStackTrace()函数
指出异常的类型、性质、栈层次及出现在程序中的位置。
异常类分为系统定义的异常和用户自定义的异常。
1. 系统定义的异常
图2中Exception派生的这些子类都是系统事先定义好并包含在Java类库中的,系统定义的异常对应着一些系统错误,如中断、文件没找到等错误。下表列举了一些常见的系统异常类。
系统已有的异常类的使用方法:
try{
程序代码
}catch(IOException ioe){
程序代码
}catch(SQLException sqle){
程序代码
}finally{
程序代码
}
2. 用户自定义异常
使用Java内置的异常类可以描述在编程时出现的大部分异常情况。除此之外,用户还可以自定义异常。用户自定义异常类,只需继承Exception类即可。例如:
public classUserDefineException extends Exception
{
publicUserDefineException(String msg)
{
super(msg);
}
};
在程序中使用自定义异常类,大体可分为以下几个步骤。
(1)创建自定义异常类。
(2)在方法中通过throw关键字抛出异常对象。
(3)如果在当前抛出异常的方法中处理异常,可以使用try-catch语句捕获并处理;否则在方法的声明处通过throws关键字指明要抛出给方法调用者的异常,继续进行下一步操作。
(4)在出现异常方法的调用者中捕获并处理异常
三、异常处理
Java异常通常在调用某些方法不一定完全成功时抛出,针对抛出的异常程序需要给出相应的处理,这称为异常处理。异常处理分为三个部分:捕捉异常、程序流程的跳转和异常处理语句块。
当一个异常被抛出时,程序中有专门的语句来接收这个被抛出的异常对象,这个过程就是捕捉异常;当一个异常类的对象被捕捉或接收后,用户程序就会发生流程跳转,系统中止程序运行,跳转到异常处理语句的执行,或者直接跳出系统,回到操作系统状态下。在Java语言中,try语句用来启动Java的异常处理机制,通常是可能抛出异常的语句的调用;而catch语句进行捕捉和处理异常,有时添加finally语句块,finally中的语句是正常执行或者处理异常之后必须执行的语句。
Java异常的捕获和处理是一个不容易把握的事情,如果处理不当,不但会让程序代码的可读性大大降低,而且导致系统性能低下,甚至引发一些难以发现的错。
Java异常处理涉及到五个关键字,分别是:try、catch、finally、throw、throws。下面将逐一介绍,通过认识这五个关键字,掌握基本异常处理知识。
(1)try:它里面放置可能引发异常的代码
(2)catch:后面对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块,可以有多个catch块。
(3)finally:主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件),异常机制总是保证finally块总是被执行。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
(4)throw:用于抛出一个实际的异常,可以单独作为语句使用,抛出一个具体的异常对象。
(5)throws:用在方法签名中,用于声明该方法可能抛出的异常。
throw与throws关键字的区别
(1)throw关键字是用于方法体内部,用来抛出一个Throwable类型的异常。如果抛出了检查异常,则还应该在方法头部声明方法可能抛出的异常类型。该方法的调用者也必须检查处理抛出的异常。如果所有方法都层层上抛获取的异常,最终JVM会进行处理,处理也很简单,就是打印异常消息和堆栈信息。如果抛出的是Error或RuntimeException,则该方法的调用者可选择处理该异常。
(2)throws关键字用于方法体外部的方法声明部分,用来声明方法可能会抛出某些异常。仅当抛出了检查异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣一般在catch块中打印一下堆栈信息做个勉强处理。
1. 直接抛出异常
例如:
publicvoid myMethod()
{
try
{
urlName = newURL("http://www.sina.com.cn");
getAppletContext().showDocument(urlName,"right");
}
catch(MalformedURLException e)
{
System.out.println(e.getMessage());
}
}
try部分试图打开一个网址http://www.sina.com.cn,如果不成功,比如网址输入错误,那么引发异常程序忽略try中的下一行代码,直接跳到catch块中执行语句System.out.println(e.getMessage()),打印出引发的异常的错误信息描述。其中MalformedURLException说明引发的异常可能是URL网址输入错误类。
2. 间接抛出异常
例如:
publicvoid myMethod() throws MalformedURLException
{
urlName = newURL("http://www.sina.com.cn");
getAppletContext().showDocument(urlName,"right");
}
在方法后面直接抛出。
3. 综合方法
例如:
publicvoid myMethod() throws MalformedURLException
{
try
{
urlName = new URL("http://www.sina.com.cn");
getAppletContext().showDocument(urlName,"right");
}
catch(MalformedURLException e)
{
System.out.println(e.getMessage());
}
}
最后还应该注意一下异常处理的语法规则:
第一、try语句不能单独存在,可以和catch、finally组成 try...catch...finally、try...catch、try...finally三种结构,catch语句可以有一个或多个,finally语句最多一个,try、catch、finally这三个关键字均不能单独使用。
第二、try、catch、finally三个代码块中变量的作用域分别独立而不能相互访问。如果要在三个块中都可以访问,则需要将变量定义到这些块的外面。
第三、多个catch块时候,Java虚拟机会匹配其中一个异常类或其子类,就执行这个catch块,而不会再执行别的catch块。
第四、throw语句后不允许有紧跟其他语句,因为这些没有机会执行。
第五、如果一个方法调用了另外一个声明抛出异常的方法,那么这个方法要么处理异常,要么声明抛出。
四、多异常的处理
多异常的处理使用多个catch来捕捉不同类的异常,Java中对catch块的数量没有限制。格式如下:
Try
{
...
}
catch(异常类1 标识符)
{
...
}
catch(异常类2 标识符)
{
...
}
finally
{
...
}
【例】测试多异常处理和自定义异常。源程序代码如下:
//程序文件名UseMultiException.java
public classUseMultiException extends Exception
{
static String str1;
static String str2;
public static voidmain(String[] args)
{
int i1 = 0;
int i2 = 0;
str1 = new String(args[0]);
str2 = new String(args[1]);
String strResult = newString();
try
{
read(str1, str2);
i1 =Integer.parseInt(str1);
i2 =Integer.parseInt(str2);
int result = i1/i2;
strResult =String.valueOf(result);
}
catch(NumberFormatExceptione)
{
strResult = "错误的数字:" + e.getMessage();
}
catch(ArithmeticException e)
{
strResult = "被0除错误:" + e.getMessage();
}
str1 = s1.trim();
str2 = s2.trim();
if(str1.equals("0"))
{
throw (new UserDefineException("本系统0不能为被除数"));
}
}
};
classUserDefineException extends Exception
{
public UserDefineException(Stringmsg)
{
super(msg);
}
};
具体运行测试如下,结果如图7.2所示。
javaUseMultiException 2gf 5
数字格式出错。
javaUseMultiException 2gf 0
数字格式出错,下面的程序代码不再执行,无法抛出第二个异常。
javaUseMultiException 2 0
被0除出错。
javaUseMultiException 0 3
引发用户自定义错误。
javaUseMultiException 9 3
9/3的答案为3。
图3 异常测试输出
五、Java异常处理的原则和技巧
1、避免过大的try块,不要把不会出现异常的代码放到try块里面,尽量保持一个try块对应一个或多个异常。
2、细化异常的类型,不要不管什么类型的异常都写成Excetpion。
3、catch块尽量保持一个块捕获一类异常,不要忽略捕获的异常,捕获到后要么处理,要么转译,要么重新抛出新类型的异常。
4、不要把自己能处理的异常抛给别人。
5、不要用try...catch参与控制程序流程,异常控制的根本目的是处理程序的非正常情况。