异常
在理想状态下,我们认为用户的输入永远是正确的,选择打开的文件也是一定存在的,并且永远不会出现BUG,但这仅仅是理想状态。但现实状况却充满了不良的数据和带有问题的代码。为了避免这类事情的发生,至少应该做到以下几点:
①向用户通告错误
②保存所有的工作结果
③允许用户以妥善的形式退出程序
(一)异常分类
在Java中,异常对象都是派生于Throwable类的一个实例,如果Java中内置的异常类不能满足需求,用户可以创建自己的异常类。
需要注意的是,Java中的异常都是由Throwable继承而来,但在下一层立即分解为两个分支:Error和Exception。
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这类对象。如果出现了这样的内部错误,除了告知用户,并尽力使程序安全退出外,再也无能为力了。这种情况很少出现。
Exception类层次结构又分为两支,一支是RuntimeException,另一个是其他异常。划分两支的规则是:由于程序错误导致的异常是RuntimeException;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
派生于RuntimeException的异常包含以下几种情况:
- 错误的类型转换
- 数组访问越界
- 访问null指针
不是派生于RuntimeException的异常包括:
- 试图在文件尾部后面读取数据
- 试图打开一个不存在的文件
- 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在
“如果出现RuntimeException异常,那么就一定是你的问题。”是一条相当有道理的规则。
Java语音规范将派生于Error类或RuntimeException类的所有异常称为非受查(unchecked)异常,所有其他的异常称为受查(checked)异常。编译器将核查是否为所有的受查异常提供了异常处理器。
(二)声明受查异常
如果遇到了无法处理的情况,那么Java方法可以抛出一个异常。这个道理很简单,一个方法不仅要告诉编译器需要返回一个什么值,还要告诉编译器有可能发生什么错误。
方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类受查异常。看个例子:
public FileInputStream(String name) throws FileNotFoundException
我们看这个声明表示这个构造器将根据给定的String参数产生了一个FileInputStream对象,但也可能抛出一个FileNotFoundException异常。如果发生了这种糟糕的情况,构造器将不会初始化一个新的FileInputStream对象,而是抛出一个FileNotFoundException对象。如果这个方法这的抛出了一个异常对象,运行时系统就会开始搜索异常处理器,以便知道如何处理FileNotFoundException对象。
在自己写方法时,什么时候需要在方法中用throws子句声明异常,什么异常必须使用throws子句声明,需要记住以下几种情况需要抛出异常:
①调用一个抛出受查异常的方法
②程序运行过程中发现错误,并且利用throw语句抛出一个受查异常
③程序出现错误
④Java虚拟机和运行时库出现的内部错误
如果出现前两种情况之一,就必须告诉调用这个方法的程序员有可能抛出异常。因为任何一个抛出异常的方法都有可能是一个死亡陷阱。如果处理器没有捕获这个异常,当前执行的线程就会结束。
对于那些可能被他人使用的Java方法,应该根据异常规范(exception specification),在方法的首部声明这个方法可能抛出的异常。
class MyAnimation{
...
public Image loadImage(String s) throws IOException{
...
}
}
如果一个方法有可能抛出多个异常,那么就要在方法的首部列出所有的异常类,每个异常类用逗号隔开。
class MyAnimation{
...
public Image loadImage(String s) throws FileNotFoundException,IOException{
...
}
}
但是不需要声明Error,因为我们对其没有控制能力,也不应该声明从RuntimeException继承的那些非受查异常。
总之,一个方法必须声明所有可能抛出的受查异常,那些非受查异常,要么不可控(Error),要么就应该避免发生(RuntimeException)。如果方法没有声明所有可能发生的受查异常,编译器就会发出一个错误消息。
警告:在继承中,如果子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用。(也就是说子类要声明更特定的异常或者不抛出任何异常)。如果超类方法没有抛出异常,那么子类方法也不能抛出任何异常。
(三)如何抛出异常
对于一个已经存在的异常类,要抛出一个异常很简单:
①找到一个合适的异常类
②创建这个类的对象
③将对象抛出
throw new EOFException;
或者是
EOFException e = new EOFException();
throw e;
一旦方法抛出了异常,这个方法就不可能返回到调用者。也就是说,不必为返回的默认值或错误代码担忧。
(四)创建异常类
当标准库中没有异常类能够充分的描述清楚问题,那么我们可以自定义异常类。自定义异常类需要做的只是定义一个派生于Exception的类,或者派生于Exception类的子类。
class FileFormatException extends IOException
{
public FileFormatException(){}
public FileFormatException(String gripe){
super(gripe);
}
}
习惯上,定义的类应该包含两个构造器,一个是默认的构造器;另一个是带有详细描述信息的构造器。(超类Throwable的toString方法将会打印出这些详细信息,这个在调试中非常有用)。
现在可以抛出自己定义的异常类型了:
String readData(BufferedReader in) throws FileFormatException
{
...
while(...){
if(ch==-1)//EOF{
if(n<len){
throw new FileFormatException();
}
}
}
return s;
}
捕获异常
(一)捕获异常
如果某个异常发生的时候没有在任何地方进行捕获,那么程序就会终止,并在控制台打印出异常信息,其中包括异常的类型和堆栈的内容。
要想捕获一个异常,必须设置try/catch语句块。
try
{
code
more code
more code
}
catch (ExceptionType e)
{
handler for this type
}
如果在try语句块中的任何代码抛出了一个在catch子句中说明的异常类,那么
①程序将跳过try语句块中的其余代码
②程序将执行catch子句中的处理器代码
如果在try语句中没有抛出任何异常,那么程序将跳过catch子句。下面看个例子:
public void read(String filename)
{
try
{
InputStream in = new FileInputStream(filename);
int b;
while((b=in.read())!=-1)
{
process input
}
}
catch(IOException exception)
{
exception.printStackTrace();
}
}
对于一个普通的程序来说,这样处理异常基本上合乎情理。
通常,最好的选择是什么也不做,而是将异常传递给调用者。如果read方法出现了错误,就让read方法的调用者去操心。如果采用这种处理方式,就必须声明这个方法会抛出一个IOException。
public void read(String filename) throws IOException
{
InputStream in = new FileInputStream();
int b;
while((b=in.read))!=-1)
{
process input
}
}
请记住,编译器严格地执行throws说明符。如果调用了一个抛出受查异常的方法,就必须对它进行处理,或者继续传递。
前面提到过如果超类方法没有抛出任何异常,那么这个方法也不能抛出任何异常,只能捕获方法代码中出现的每一个受查异常。
(二)捕获多个异常
在一个try语句块中,可以捕获多个异常类型,并对不同类型的异常做出不同的处理。
try
{
code that might throw exceptions
}
catch(FileNotFoundException e){
emergency action for missing files
}
catch(UnknownHostExcption e){
emergency action for unknown hosts
}
catch(IOException e){
emergency action for all other I/O problems
}
在Java7中,同一个catch子句中可以捕获多个异常类型。假如FileNotFoundException和UnknownHostException的动作是一样的,就可以写成:
try
{
code that might throw exceptions
}
catch(FileNotFoundException | UnknownHostException e)
{
emergency action for missing files and unknown hosts
}
catch(IOException e)
{
emergency action for all other I/O problems
}
异常对象可能包含与异常本身有关的信息,想要获得对象的更多信息,可以试着使用e.getMessage()得到详细的错误信息,或者使用e.getClass().getName()得到异常对象的实际类型。
(三)再次抛出异常与异常链
在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。下面给出了捕获异常并将它再次抛出的基本方法:
try
{
access the database
}
cathc(SQLException e)
{
throw new ServletException("database error : "+e.getMessage());
}
不过这里也有一种更好的处理方法,并且将原始异常设置为新异常的“原因”:
try
{
access the database
}
catch(SQLException e)
{
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
当捕获到异常时,就可以使用下面这条语句重新得到原始异常:
Throwable e = se.getCause();
强烈建议使用这种包装技术,这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
(四)finally子句
当代码抛出一个异常时,就会终止方法中剩余代码的处理,并且退出这个方法的执行。如果方法获得了一些本地的资源,只有这个方法自己知道。又如果这些资源在退出方法前必须被回收,那么就会产生资源回收问题。
Java中有一种好的解决方案,就是finally子句。不管是否有异常被捕获,finally子句中的代码都被执行。我们看看下面的示例:
InputStream in = new FileInputStream(...);
try
{
//1
code that might throw exceptions
//2
}
catch(IOException e)
{
//3
show error message
//4
}
finally
{
//5
in.close();
}
//6
在上面这段代码中,有下列3种情况会执行finally子句:
(1)代码没有抛出异常。这种情况下,会执行try语句块里全部代码,然后执行finally子句中的代码,全部执行完后就会执行try语句块后第一条语句。也就是说:执行标注的1、2、5、6。
(2)抛出一个在catch子句中捕获的异常。先执行try语句块里的代码,当异常发生的时候,会跳过try语句块里的剩余代码,然后执行catch子句中的代码,最后执行finally子句中的代码块。也就是1、3、4、5、6。
如果catch子句中没有抛出新的异常,异常将被抛回这个方法的调用者。在这里,执行1、3、5处的语句。
(3)代码抛出了一个异常,但这个异常不是由catch子句捕获的。在这种情况下,程序将执行try语句块中的所有语句,直到有异常被抛出为止。此时,将跳过try语句块中的剩余代码,然后执行finally子句中的语句,并将异常抛给这个方法的调用者。在这里,执行1、5处的语句。
try语句块也可以只有finally子句,而没有catch子句。例如:
InputStream in = new FileInputStream(...);
try
{
code that might throw exceptions
}
finally
{
in.close();
}
事实上,我们认为在需要关闭资源时,用这种方式使用finally子句是一种不错的选择。这里,强烈建议解耦合try/catch和try/finally语句块。这样可以提高代码的清晰度。例如:
//这种方式不仅设计清楚,还有一个功能就是将会报告finally子句中出现的错误
try //外层try语句块也只有一个职责,就是确保报告出现的错误
{
try //内层try语句块只有一个职责,就是确保关闭输入流
{
code that might throws exceptions
}
finally
{
in.close();
}
}
catch(IOException e)
{
show error message
}
(五)带资源的try语句
try(Resource res = ...)
{
work with res
}
使用这种方式,try块退出时,会自动调用res.close()。例如:
try(Scanner in = new Scanner(new FileInputStream("usr/share/dict/words")),"UTF-8")
{
while(in.hasNext()){
System.out.println(in.next());
}
}
也可以指定多个资源,例如:
try(Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words","UTF-8");PrintWriter out = new PrintWriter("out.txt"))
{
while(in.hasNext()){
out.println(in.next().toUpperCase());
}
}
不论这个块如何退出,in和out都会关闭。所以只要需要关闭资源,就要尽可能使用带资源的try语句。
(六)分析堆栈轨迹元素
堆栈轨迹(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。我们可以通过两种方法来显示这个列表:
①可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
②一种更灵活的方法是使用getStackTrace方法,它会得到StackTraceElement对象的一个数组,可以在你的程序中分析这个对象数组。
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement frame : frames)
{
analyze frame
}
③静态的Thread.getAllStackTrace方法,它可以产生所有线程的堆栈轨迹。下面就是多线程时,使用这个方法的具体方式:
Map<Thread,StackTraceElement[]> map = Thread.getAllStackTraces();
for(Thread t : map.keySet())
{
StackTraceElement[] frames = map.get(t);
analyze frames
}
下面的程序打印了递归阶乘函数的堆栈情况。
package stackTrace;
import java.util.Scanner;
public class StackTraceTest {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.println("Enter n : ");
int n = in.nextInt();
factorial(n);
}
public static int factorial(int n) {
System.out.println("factorial("+n+"):");
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement frame : frames) {
System.out.println(frame);
}
int r;
if(n<=1) {
r=1;
}else {
r=n*factorial(n-1);
}
System.out.println("return "+r);
return r;
}
}