82.处理异常
1.常见异常
下面列出了一些常见的异常:
RuntimeException
ArithmeticException:数学计算异常
ArrayIndexOutOfBoundsException:数组越界异常
NullPointerException:空指针异常
NegativeArraySizeException:负数组长度异常
ClassCastException:造型异常
IllgalArgumentException:非法参数值异常
IllegalStateException:对象状态异常,如对未初始化的对象调用方法
UnsupportedOperationException:对象不支持的操作异常,如调用方法名、方法参数写错等。
IOException
FileNotFoundException:指定文件未找到异常
EOFException:读写文件尾异常
MalformedURLException:URL格式错误异常
SocketException:Socket通信异常
其他异常:
ClassNotFoundException:无法找到需要的类文件异常
2 Java中的异常处理机制
Java程序的执行过程中如出现异常,会自动生成一个异常类对象,该异常对象将被提交给Java运行时环境,这个过程称为抛出(throw)异常。
当Java运行时环境接收到异常对象时,会寻找能处理这一异常的代码并把当前异常对象交给其处理,这一过程称为捕获(catch)异常。
如果Java运行时环境找不到可以捕获异常的方法,则运行时环境将终止,相应的Java程序也将退出。
正如我们前面所言,程序员通常只能处理异常(Exception),而对错误(Error)无能为力。
3 通过try-catch-finally来处理异常
如果一个非图形化的应用程序发生了异常,并且异常没有被处理,那么,程序将会中止运行并且在控制台(如果是用控制台启动的应用的话)输出一条包含异常类型以及异常堆栈(Stack)内容的信息;而如果一个图形化的应用程序如果发生了异常,并且异常没有被处理,那么,它也将在控制台中输出一条包含异常类型和异常堆栈内容的信息,但程序不会中止运行。所以,对于异常,需要作出相应的处理。其中一种方法就是将异常捕获,然后对被捕获的异常进行处理,在Java中,可以通过try-catch-finally语句来捕获异常:
try{
// 可能会抛出特定异常的代码段
}[catch(MyExceptionType myException){
// 如果myException 被抛出,则执行这段代码
}catch(Exception otherException){//如果另外的异常otherException被抛出,则执行这段代码
}] [finally{
//无条件执行的语句
}]
也就是说,对于异常的处理语句可能为下面三种中的一种:
try-catch[-catch…]
try-catch[-catch…]-finally
try-finally
通过try-catch语句,可以将可能出现的异常通过catch()子句捕获并在相应的地方处理,另外还可以加入一个finally子句,在finally子句中的代码段无论是否发生异常都将被无条件执行。
异常处理可以定义在方法体、*块或构造器中。并且,try-catch-finally语句可以嵌套使用。
将可能出现异常的代码都放在try代码块中,当然,也可以将其他的一些不会引起异常的代码也一并放到try代码块中。
catch() 从句中引入一个可能出现的异常,一个try块可以和多个catch()块配合以处理多个异常。
当try块内的任何代码抛出了由catch() 子句指定的异常,则try代码段中的程序将会终止执行,并跳到相应的catch()代码块中来执行。可以通过Exception的getMessage()方法来获得异常的详细信息或者通过printStackTrace()方法来跟踪异常事件发生时执行堆栈的内容。
无论是否出现异常,程序最后都会执行finally代码块中的内容。finally的意义在于,无论程序如何运行,它都必然会被执行到。如果在try从句中给方法分配了一些资源(比如,数据库连接、打开一个文件、网络连接等),然后,方法出现异常,它将会抛出一个异常,方法中的未执行的代码将会中止执行,并转而执行catch()从句中的内容,这个时候,本来定义在try从句中的资源回收动作就不会执行了,这就会导致资源没有回收的情况。此时,就可以将资源回收的动作放到finally从句中来执行,无论是否会有异常发生,它都能被执行到。
下面我们来看一个使用try-catch执行异常捕获的例子。
import java.io.*;
public class CatchException {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("c:/a.txt");
int b;
b = fis.read();
while (b != -1) {
System.out.print((char) b);
b = fis.read();
}
fis.close();
} catch (FileNotFoundException e) {
System.out.println("FileNotFoundException:" + e.getMessage());
} catch (IOException e1) {
System.out.println("IOException:" + e1.getMessage());
}
}
}
这个例子是前面的ExceptionExam2的完整版本。在这个程序中,必须要捕获两个异常:FileNotFoundException和IOException。如果出现了这两个异常的任何一个,系统都将会执行各自catch()代码段中代码:都是通过getMessage()方法来得到出现异常的详细信息。
我们将指定的文件(此处是c盘下的a.txt)删除或改成其他的名字,然后执行这个程序,将会在控制台上得到类似如下的结果:
FileNotFoundException:c:\a.txt (系统找不到指定的文件。)
这说明,在执行这个程序的时候,发生了FileNotFoundException异常,因此程序已经转到catch(FileNotFoundException)指定的代码段中执行了。
如果指定的文件(c盘下的a.txt)存在,并且在读取这个文件的时候没有出现 IO异常,则程序将从文件“a.txt”中读出文件内容并一行行的输出到控制台中。
在通过catch来捕获多个异常时,越“具体”的异常放在越前面,也就是说,如果这些异常之间有继承关系,则应该将子类的异常放在前面,而将父类的异常放在后面来捕获。比如,上面两个异常IOException和FileNotFoundException就存在继承关系,FileNotFoundException是IOException的子类,所以只能将FileNotFoundException放在IOException之前捕获,而将IOException放在后面。
这主要是因为,如果将IOException放在前面,则程序运行的时候如果碰到 FileNotFoundException,它会被IOException这个子句捕获,那么,FileNotFoundException子句就永远也不会被执行了。将FileNotFoundException放在IOException之后编译程序将会出现下面的错误:
ExceptionExam1.java:23: exception java.io.FileNotFoundException has already been caught
catch(FileNotFoundException e)
^
1 error
但是,这个代码还有一些问题,就是我们前面讨论的关于资源回收的问题:在方法main()中,打开了一个文件,这就占用了这个资源。在理想状态下,它应该在方法执行的最后被关闭:
fis.close();
但是,如果在方法的执行过程中发生了IOException(通常由于fis.read()引起),那么,方法将会退出try从句中的语句的执行。那么,关闭打开的文件那条语句就不会被执行,这样,方法占用的资源就无法被释放。此时,可以通过将关闭文件的语句放到finally从句中去,这样,无论是否会发生异常,它都会被执行,也就解决了资源可能无法释放的问题。
刚才讨论的是发生IOException的情况,那么,如果方法发生FileNotFoundException的时候,会发生什么情况呢?如果发生FileNotFoundException,也就没有文件被打开,那么,如果此时在finally从句中执行fis.close()语句,肯定会出现问题:它试图关闭一个并不存在的文件流。因此,在执行fis.close()的地方,也需要做异常处理:使用try-catch语句来捕获这种异常。事实上,FileInputStream的close()方法必须要对它作异常处理。
下面我们来看这个完整的程序。
import java.io.*;
public class CatchException {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("c:/a.txt");
int b;
b = fis.read();
while (b != -1) {
System.out.print((char) b);
b = fis.read();
}
// 移到finally从句中去执行
// fis.close();
} catch (FileNotFoundException e) {
System.out.println("FileNotFoundException:" + e.getMessage());
} catch (IOException e1) {
System.out.println("IOException:" + e1.getMessage());
} finally {
try {
if (fis != null)
fis.close();
} catch (IOException ioe) {
System.out.println("关闭文件出错!");
}
}
}
}
注意,在这个程序中,如果将fis.close()语句移到finally中去执行,那么,变量fis的声明必须从try从句中移出来,因为如果try从句中的变量fis定义语句没有被执行的话,变量fis是不存在的,那么,在finally中就不能使用fis这个变量,所以,需要将变量fis的声明放到try从句外面来,并且给它一个初始值null。
另外,try语句可以不用catch而直接和finally结合使用处理异常情况。
注意:千万不要对捕获的异常“不作为”。也就是说,不要让catch()从句中的代码段是空的:try{ …}catch(Exception e){}
上面这段代码中的catch()从句中,对捕获的异常什么都没有做,那么,当你调试程序的时候,如果程序出现了异常,你也无法知道,因为程序将异常捕获后,没有做任何的提示或者其他处理,程序中其他的语句就会像没有发生异常一样继续执行,但是,这个时候执行的结果往往已经不是你所期望的结果了,这种做法往往给调试程序带来很大的困扰。
而如果在一个发布的应用程序中出现这种情况,用户也往往会得到并不期望的结果但还不知道因为程序中出现了问题,程序运行结果已经不正确了。
另外,如果方法有返回值,那么,一定要注意是否在try和finally中都有return语句,很多Java新手在使用try[-catch]-finally来捕获异常的时候,容易想当然的以为只要有return语句,就会马上离开正在执行的方法。而事实的真相是:如果有finally从句存在,它就一定会被执行。我们来考虑下面的代码:
public int getInt(){
try {
return 1;
}
//catch(Exception e){
// return 2;
//}
finally
{
return 3;
}
}
上面的这个方法getInt()将返回什么?(因为没有异常会发生,所以将catch()注释了)答案是3。或许按照你的设计,你期望它返回1(“程序没有异常发生的时候,将运行try从句中的代码,也就会返回try中的return值”),但事实却非你所想象,它的finally中的返回值3覆盖了try从句中的返回值1。
所以,一定要注意,在finally从句中,是否有可能改变返回值的语句,如果有,应该删除或做其他妥善的处理。
当然,在一种情况下finally不会被执行,那就是在try从句或者catch()从句中有System.exit()语句的时候,此时整个程序将退出执行,finally从句也就无法被执行了。
4 将异常抛出
在定义一个方法的时候,可以不在方法体中对异常进行处理,而是将可能发生的异常让调用这个方法的代码来处理。这是通过所谓的“抛出异常”来实现的。
可以对下列情形在方法定义中抛出异常:
方法中调用了一个会抛出“已检查异常”的方法;
程序运行过程中发生了错误,并且用throw子句抛出了一个“已检查异常”。
不要抛出如下异常:
从Error中继承来的那些错误;
从RuntimeException中派生的那些异常,如NullPointerException等。
如果一个异常没有在当前的try-catch模块中得到处理,则它会抛出到它的调用方法。
如果一个异常回到了main()方法,仍没有得到处理,则程序会异常终止。
方法中抛出异常的格式:
<modifer> <returnType> methodName([<argument_list>]) throws <exception_list> {
//……
}
前面已经说过,哪种类型的异常应该在方法中被抛出,而哪些不应该被抛出。简而言之,就是一个方法应该抛出它可能碰到的所有的“已检查异常”,而对于“未检查异常”和Error,应该通过程序来避免这种情况的发生,比如,检查对象引用是否为空避免空指针异常、检查数组大小避免数组越界访问异常等。
import java.io.*;
public class ThrowExam {
public void readFile() throws FileNotFoundException, IOException {
FileInputStream fis = new FileInputStream("c:/a.txt");
int b;
b = fis.read();
while (b != -1) {
System.out.print((char) b);
b = fis.read();
}
fis.close();
}
public static void main(String[] args) {
ThrowExam te = new ThrowExam();
try {
te.readFile();
} catch (FileNotFoundException e) {
System.out.println("FileNotFoundException:" + e.getMessage());
} catch (IOException e1) {
System.out.println("IOException:" + e1.getMessage());
}
}
}
在这个例子中,定义了一个方法readFile()用于读取指定文件的内容,它可能会引起FileNotFoundException和IOException,我们没有直接在这个方法中对它们进行处理,而是将这两个异常抛出,让这个方法的调用者来处理。比如,在main()方法中调用了这个方法,那么这个时候就需要对它们进行处理了。当然,也可以将这两个异常再抛出给main()方法的调用者。但不建议将异常抛给main()方法的调用者来处理。因为根据Java的调用栈机制,如果一个异常回到了main()方法,仍没有得到处理,则程序会异常终止。
这个例子是抛出异常中两种情形中的一种:方法中调用了一个会抛出“已检查异常”的方法。
我们再来看一下抛出异常的另一个情形:程序运行过程中发生了错误,并且用throw子句抛出了一个“已检查异常”。
import java.io.*;
public class ThrowExam1 {
public void readFile() throws FileNotFoundException, IOException {
File f = new File("c:/a.txt");
if (!f.exists()) {
throw new FileNotFoundException("File can't be found!");
}
FileInputStream fis = new FileInputStream(f);
int b;
b = fis.read();
while (b != -1) {
System.out.print((char) b);
b = fis.read();
}
fis.close();
}
public static void main(String[] args) {
ThrowExam1 te = new ThrowExam1();
try {
te.readFile();
} catch (FileNotFoundException e) {
System.out.println("FileNotFoundException:" + e.getMessage());
} catch (IOException e1) {
System.out.println("IOException:" + e1.getMessage());
}
}
}
在这个程序中,我们使用File对象来作为FileInputStream这个类的构造器的参数。因为File类中有一个用于判断文件或目录是否存在的方法exists(),所以,可以使用这个方法来判断指定文件是否存在,如果存在,则肯定不会抛出FileNotFoundException,而如果不存在,则一定会抛出FileNotFoundException异常,通过这个现成的异常类,我们可以控制异常的抛出时机。
请注意程序中的斜体部分,它首先判断指定的文件是否存在,如果不存在,将抛出一个FileNotFoundException。注意,在这边使用throw关键字抛出对象的时候,抛出的必须是Throwable或者它的子类的实例,而不能是其他的任何类型的对象,当然,更不可以是简单类型数据。
因此,如果一个现成的异常可以使用,则我们可以方便的自己来抛出异常:
1.找到一个合适的异常类;
2.实例化这个异常类;
3.抛出这个异常类对象。
在实例化一个用于抛出的异常类的时候,通常使用这些异常类的带String类型参数的构造器,使用这个构造器,可以更加精确的描述异常发生的情况:
FileNotFoundException fne
= new FileNotFouneException("File "+filename+ " Not Found!");
throw fne;
或者:
FileNotFoundException fne
= new FileNotFoundException("文件"+fileName+"没有找到!");
throw fne;
5 捕获异常和抛出异常结合使用
当我们捕获异常但不知道如何去处理这些异常时,我们可以将捕获的异常抛给方法调用者来处理它。捕获异常和抛出异常的方式,并不是排它的,它们可以结合起来使用:
method() throws XXXException{
try{…}
catch(XXXException e) {
throw e;
}
}
在catch()从句中,可以向外抛出被捕获的异常类型的实例,也可以向外抛出另外一个类型的异常的实例:
method() throws XXXException{
try{…}
catch(XXXException e) {
throw new Exception("My Exception");
}
}
6 进行方法覆盖时对异常的处理
当子类中的方法覆盖父类中的方法时,可以抛出异常。
覆盖方法抛出的异常,可以抛出与被覆盖方法的相同的异常或者被覆盖方法的异常的子类异常。
import java.io.*;
public class Parent {
public void methodA()
throws IOException {
// IO操作
}
}
import java.io.*;
public class Child extends Parent {
public void methodA()
throws FileNotFoundException, UTFDataFormatException{
// IO操作,数学运算
}
}
在这个案例中定义了两个类:Parent和Child,其中Child是Parent的子类。在Child这个子类中,覆盖了父类中的methodA()方法,请注意它抛出的异常和父类中被覆盖方法抛出的异常间的关系。FileNotFoundException和UTFDataFormatException是IOException类的子类。这样的覆盖方法是允许的。
再来看一个也是继承了Parent类,并且覆盖了方法methodA()的例子:
import java.io.*;
public class Child1 extends Parent {
public void methodA()
throws Exception {
// IO操作,数学运算
}
}
编译这个程序,将会出错:
Child1.java:4: methodA() in Child1 cannot override methodA() in Parent; overridden method does not throw java.lang.Exception
public void methodA()
^
1 error
这就是因为覆盖方法抛出的异常不是被覆盖方法的异常或者它的子类。相反的,Exception是IOException的父类。
另外,如果父类方法没有声明抛出异常,那么子类覆盖方法不可以声明抛出“已检查”异常,也不能抛出除父类方法中声明的异常(包括其子类异常)外的其他“已检查”异常。
7 自定义异常
虽然JDK中包含了丰富的异常处理类,但是,很多时候,我们不得不借助自己定义的异常处理类来处理异常。通过继承Exception或者它的子类,就可以实现自己的异常类。
一般而言,在实现自己定义的异常类时,会给这个异常类设计两个构造器:一个参数为空的构造器以及一个带一个String类型参数的构造器,用来传递详细的出错信息。
public class MyDivideException extends ArithmeticException {
public MyDivideException() {
super();
}
public MyDivideException(String msg) {
super(msg);
}
public String toString() {
return "除以零引起的例外!";
}
}
在这个自定义的异常类中,定义了两个构造器,并且覆盖了父类中的toString()方法,使得这个方法能够返回更能反映这个类的信息。
public class DivideExceptionTest {
public static void main(String args[]) {
int n = 0, d = 0;
double q;
try {
n = Integer.parseInt(args[0]);
d = Integer.parseInt(args[1]);
if (d == 0)
throw new MyDivideException();
q = (double) n / d;
System.out.println(n + "/" + d + "=" + q);
} catch (MyDivideException e) {
System.out.println(e);
}
}
}
这个类中,使用到了我们自己定义的那个异常类:MyDivideException。如果我们通过下列命令来运行这个程序:
java DivedeExceptionTest 1 0
则将会得到一个输出如下:
除以零引起的例外!
8 通过printStackTrace()追踪异常源头
利用Exception的printStackTrace()方法可以追踪异常出现的执行堆栈情况,并可以以此找到异常的源头。
public class SelfDefinedException extends Exception{
public SelfDefinedException(){
super("自定义的例外类");
}
}
这是一个自定义的异常类,并没有实际的用途,仅用于演示printStackTrace()方法。
public class TestPrintStackTrace{
public static void main(String args[]) {
try {
firstMethod();
}catch(SelfDefinedException e){
e.printStackTrace();
}
}
public static void firstMethod() throws SelfDefinedException{
secondMethod();
}
public static void secondMethod() throws SelfDefinedException{
thirdMethod();
}
public static void thirdMethod() throws SelfDefinedException{
throw new SelfDefinedException();
}
}
在这个类中,定义了三个方法,第一个方法调用第二个方法,第二个方法调用第三个方法,而第三个方法只是抛出了一个异常,当运行这个程序时,将会得到如下的输出:
SelfDefinedException: 自定义的例外类
at TestPrintStackTrace.thirdMethod(TestPrintStackTrace.java:32)
at TestPrintStackTrace.secondMethod(TestPrintStackTrace.java:27)
at TestPrintStackTrace.firstMethod(TestPrintStackTrace.java:22)
at TestPrintStackTrace.main(TestPrintStackTrace.java:12)
由此,可以看出执行main()方法中出现了一个SelfDefinedException异常,而这个异常的源头在thirdMethod中。
提示:
虽然printStackTrace()方法可以很方便的用于追踪异常的发生情况,可以使用它来调试程序,但在最后发布的程序中,应该避免使用它,而应该对捕获的异常进行适当的处理,而不是简单的将异常堆栈打印出来。
下一篇: 87.String类