没有人敢保证说它写的程序永远没有错。即使写的程序没有错,也不要指望你的用户能按照你的意愿来执行程序,比如,你不要指望用户的网络是畅通的,你不要指望你需要的某个文件一定会在它应该存在的位置,你不要期待用户一定会在需要数字的地方输入数字而不是字母甚至更奇怪的符号。
作为程序设计人员,你应该尽可能多的去想象可能会碰到的错误、尽可能糟糕地去考虑用户不规范的输入、尽可能的取考虑运行环境的恶劣,所谓“有备无患”,不要等到出了问题再去补救。
但是,且慢,我们需要针对每一个错误都自己去编写错误处理程序么?在一些编程语言中,答案也许是的,或许也没有这么糟糕,而对于Java,答案是“否”,基本上,Java将这些都替你考虑到了。
在Java中,有两个类用于处理两种方式用于处理错误:Error和Exception。
Error处理的是Java运行环境中的内部错误或者硬件问题,比如,内存资源不足等。对于这种类型错误,程序基本上是无能为力的,除了退出运行外别无它法。
而对于Exception,它处理的是因为程序设计的瑕疵而引起的问题或者外在的输入等引起的一般性问题,例如:在开平方的方法中输入了一个负数,对一个为空的对象进行操作以及网络不稳定引起的读取网络问题等,都可以通过Exception来处理。
在Java中,异常对象分为两大类:Error和Exception。
Error类和Exception类都是Throwable类的子类。Error类只有四个子类:AWTError、LinkageError、VirtualMachineError以及ThreadDeath,正如前面所述,它处理的是Java运行系统中的内部错误以及资源耗尽等情况,这种情况是程序员所无法掌握的,我们只有通知用户并安全退出程序的运行。
而Exception的子类就很多了,可以大致将它的子类分为三类:有关I/O的IOException,有关运行时的异常RuntimeException以及其他的异常。RuntimeExcepiton异常是由于程序编写过程中的不周全的代码引起的,而IOException是由于IO系统出现阻塞等原因引起的。
我们来看一个异常的例子。
源文件:ExceptionExam.java
public class ExceptionExam {
public static void main(String args[]) {
int a, b;
double c;
a = Integer.parseInt(args[0]);
b = Integer.parseInt(args[1]);
c = a / b;
System.out.println(a + "/" + b + " = " + c);
}
}
在这个程序中,我们从控制台接收两个参数,并对它们进行一个除法运算,你或许会期望用户总是能输入两个正确的操作数,但是,现实总不是你想象中的那么美好:也许,用户会输入一个0来作为除数,这个时候,情况就不如你想象的那样了,我们来
想象一下用户输入如下的命令来执行这个程序:
java ExceptionExam 12 0
这个时候,我们就会从控制台得到一个异常信息,如下:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at ExceptionExam.main(ExceptionExam.java:10)
在这个例子中,可能会因为用户输入的数据而引起数学计算的错误,它是一个RuntimeException(ArithmeticException是RuntimeException的子类)。
我们再来看一个例子。
public class ExceptionExam1 {
public static void main(String args[]) {
java.util.Date d = null;
System.out.println(d.getTime());
}
}
这个程序很简单,就是试图从一个Date对象中得到时间。运行这个程序,将会在控制台上得到如下的信息:
Exception in thread "main" java.lang.NullPointerException
at ExceptionExam1.main(ExceptionExam1.java:6)
在这个程序中,我们没有给引用变量一个真正的Date对象,因此,当试图通过这个变量调用Date对象中的方法时,将会出现一个“空指针引用”的异常。它也是一个RuntimeException(NullPointerException也是RuntimeException的子类)。
当然,上面这两个程序是很容易避免这种错误的,我们完全可以在调用它的方法之前判断,如判断除数参数是否为0,若为0,就不进行除法运算;或者首先判断对象是否为null,再决定是否调用这个对象的方法。
这种异常通常都是运行时异常,它们都有一个共同的父类RuntimeException。虽然我们可以通过程序的控制来将这种异常扼杀在程序设计阶段,但在一些更复杂的应用中,我们常常无法避免类似这种情况的出现。这个时候就可能需要使用Java的异常处理机制来对这些情况进行处理了。
还有一些异常,我们无法通过程序的控制来避免它的出现,比如,我们不太可能将客户可能输入的url地址作一个准确的判断,以决定是否进行网络连接等,这个时候,我们就必须使用Java的异常处理机制来进行处理。如果没有使用Java的异常处理机制,在编译程序的时候,编译器会强制让你必须进行处理。
例如,我们来看下面这个程序。
import java.io.*;
public class ExceptionExam2 {
public static void main(String[] args) {
FileInputStream fis = new FileInputStream("c:/a.txt");
// 其他处理代码
}
}
在这个程序中,用到了流的概念。在这里先简单将相关的FileInptStream这个类简单介绍一下。FileInputStream类通常用于从指定的文件中读取数据,它的方法read()的功能是每次从相应的(本地为ASCII码编码格式)文件中读取一个字节,并转换成0~255之间的int型整数返回,到达文件末尾时则返回-1。
这个程序试图建立一个到指定文件的输入流,但是,我们无法总是保证文件存在或者文件是可读的,因此,在这个地方应该对文件是否存在的异常进行处理,如果没有进行处理,就象上面这段代码,那么,在编译的时候,编译器将会报告一个错误:
ExceptionExam2.java:7: unreported exception java.io.FileNotFoundException; must
be caught or declared to be thrown
FileInputStream fis = new FileInputStream("c:/a.txt");
^
1 error
因此,对于一些异常,是必须进行处理的,否则编译将会出错。比如,可以将上面的代码改成如下:
import java.io.*;
public class ExceptionExam2 {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("c:/a.txt");
// 其他处理代码
} catch (FileNotFoundException e) {
// Exception handling code here
}
}
}
Java语言规范中将Error的子类和RuntimeException的子类都称为“未检查(unchecked)的异常”,而将其他的异常均称为“已检查(checked)的异常”。
对于RuntimeException,都是可以通过程序中编写代码来将这种异常避免的,比如,首先对操作数进行判断,就可以避免出现被0除这种数学计算异常,或首先对数组的长度进行判断,就不会出现数组索引(下标)越界异常了。但我们没有办法通过编写代码来避免其他“已检查异常”的出现,比如,我们没办法编写一段代码用来判断用户是否输入了合法的或者真实存在的URL。如果程序中有可能出现“已检查异常”而没有对它进行处理,则在编译的时候会发生错误。
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()方法可以很方便的用于追踪异常的发生情况,可以使用它来调试程序,但在最后发布的程序中,应该避免使用它,而应该对捕获的异常进行适当的处理,而不是简单的将异常堆栈打印出来。
作为程序设计人员,你应该尽可能多的去想象可能会碰到的错误、尽可能糟糕地去考虑用户不规范的输入、尽可能的取考虑运行环境的恶劣,所谓“有备无患”,不要等到出了问题再去补救。
但是,且慢,我们需要针对每一个错误都自己去编写错误处理程序么?在一些编程语言中,答案也许是的,或许也没有这么糟糕,而对于Java,答案是“否”,基本上,Java将这些都替你考虑到了。
在Java中,有两个类用于处理两种方式用于处理错误:Error和Exception。
Error处理的是Java运行环境中的内部错误或者硬件问题,比如,内存资源不足等。对于这种类型错误,程序基本上是无能为力的,除了退出运行外别无它法。
而对于Exception,它处理的是因为程序设计的瑕疵而引起的问题或者外在的输入等引起的一般性问题,例如:在开平方的方法中输入了一个负数,对一个为空的对象进行操作以及网络不稳定引起的读取网络问题等,都可以通过Exception来处理。
在Java中,异常对象分为两大类:Error和Exception。
Error类和Exception类都是Throwable类的子类。Error类只有四个子类:AWTError、LinkageError、VirtualMachineError以及ThreadDeath,正如前面所述,它处理的是Java运行系统中的内部错误以及资源耗尽等情况,这种情况是程序员所无法掌握的,我们只有通知用户并安全退出程序的运行。
而Exception的子类就很多了,可以大致将它的子类分为三类:有关I/O的IOException,有关运行时的异常RuntimeException以及其他的异常。RuntimeExcepiton异常是由于程序编写过程中的不周全的代码引起的,而IOException是由于IO系统出现阻塞等原因引起的。
我们来看一个异常的例子。
源文件:ExceptionExam.java
public class ExceptionExam {
public static void main(String args[]) {
int a, b;
double c;
a = Integer.parseInt(args[0]);
b = Integer.parseInt(args[1]);
c = a / b;
System.out.println(a + "/" + b + " = " + c);
}
}
在这个程序中,我们从控制台接收两个参数,并对它们进行一个除法运算,你或许会期望用户总是能输入两个正确的操作数,但是,现实总不是你想象中的那么美好:也许,用户会输入一个0来作为除数,这个时候,情况就不如你想象的那样了,我们来
想象一下用户输入如下的命令来执行这个程序:
java ExceptionExam 12 0
这个时候,我们就会从控制台得到一个异常信息,如下:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at ExceptionExam.main(ExceptionExam.java:10)
在这个例子中,可能会因为用户输入的数据而引起数学计算的错误,它是一个RuntimeException(ArithmeticException是RuntimeException的子类)。
我们再来看一个例子。
public class ExceptionExam1 {
public static void main(String args[]) {
java.util.Date d = null;
System.out.println(d.getTime());
}
}
这个程序很简单,就是试图从一个Date对象中得到时间。运行这个程序,将会在控制台上得到如下的信息:
Exception in thread "main" java.lang.NullPointerException
at ExceptionExam1.main(ExceptionExam1.java:6)
在这个程序中,我们没有给引用变量一个真正的Date对象,因此,当试图通过这个变量调用Date对象中的方法时,将会出现一个“空指针引用”的异常。它也是一个RuntimeException(NullPointerException也是RuntimeException的子类)。
当然,上面这两个程序是很容易避免这种错误的,我们完全可以在调用它的方法之前判断,如判断除数参数是否为0,若为0,就不进行除法运算;或者首先判断对象是否为null,再决定是否调用这个对象的方法。
这种异常通常都是运行时异常,它们都有一个共同的父类RuntimeException。虽然我们可以通过程序的控制来将这种异常扼杀在程序设计阶段,但在一些更复杂的应用中,我们常常无法避免类似这种情况的出现。这个时候就可能需要使用Java的异常处理机制来对这些情况进行处理了。
还有一些异常,我们无法通过程序的控制来避免它的出现,比如,我们不太可能将客户可能输入的url地址作一个准确的判断,以决定是否进行网络连接等,这个时候,我们就必须使用Java的异常处理机制来进行处理。如果没有使用Java的异常处理机制,在编译程序的时候,编译器会强制让你必须进行处理。
例如,我们来看下面这个程序。
import java.io.*;
public class ExceptionExam2 {
public static void main(String[] args) {
FileInputStream fis = new FileInputStream("c:/a.txt");
// 其他处理代码
}
}
在这个程序中,用到了流的概念。在这里先简单将相关的FileInptStream这个类简单介绍一下。FileInputStream类通常用于从指定的文件中读取数据,它的方法read()的功能是每次从相应的(本地为ASCII码编码格式)文件中读取一个字节,并转换成0~255之间的int型整数返回,到达文件末尾时则返回-1。
这个程序试图建立一个到指定文件的输入流,但是,我们无法总是保证文件存在或者文件是可读的,因此,在这个地方应该对文件是否存在的异常进行处理,如果没有进行处理,就象上面这段代码,那么,在编译的时候,编译器将会报告一个错误:
ExceptionExam2.java:7: unreported exception java.io.FileNotFoundException; must
be caught or declared to be thrown
FileInputStream fis = new FileInputStream("c:/a.txt");
^
1 error
因此,对于一些异常,是必须进行处理的,否则编译将会出错。比如,可以将上面的代码改成如下:
import java.io.*;
public class ExceptionExam2 {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("c:/a.txt");
// 其他处理代码
} catch (FileNotFoundException e) {
// Exception handling code here
}
}
}
Java语言规范中将Error的子类和RuntimeException的子类都称为“未检查(unchecked)的异常”,而将其他的异常均称为“已检查(checked)的异常”。
对于RuntimeException,都是可以通过程序中编写代码来将这种异常避免的,比如,首先对操作数进行判断,就可以避免出现被0除这种数学计算异常,或首先对数组的长度进行判断,就不会出现数组索引(下标)越界异常了。但我们没有办法通过编写代码来避免其他“已检查异常”的出现,比如,我们没办法编写一段代码用来判断用户是否输入了合法的或者真实存在的URL。如果程序中有可能出现“已检查异常”而没有对它进行处理,则在编译的时候会发生错误。
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()方法可以很方便的用于追踪异常的发生情况,可以使用它来调试程序,但在最后发布的程序中,应该避免使用它,而应该对捕获的异常进行适当的处理,而不是简单的将异常堆栈打印出来。