您的位置:首页 > 编程语言 > Java开发

java学习脚印: java异常处理机制及实践方法

2013-10-20 21:49 661 查看

java学习脚印: java异常处理机制及实践方法

写在前面
   重点把握,异常的分类,异常处理器搜索过程,应该捕获还是声明异常,以及异常处理的最佳实践。

1.程序异常概念

1.1
什么是程序异常 ?

异常是指在程序执行过程中扰乱程序正常执行流程的事件。
简单说,异常就是程序中发生的错误破坏了预定的执行过程。

1.2是什么导致异常?

理想情况下,程序运行时一切都按照程序要求执行,用户输入数据格式永远都是正确的,选择的文件一定可以读写,打印机一定处于工作状态,内存也一直够用。但是现实情况往往不可能这么理想。程序发生错误是很难避免的,当程序发生错误时即将产生异常事件。异常就来源于程序中错误代码,API使用不当,访问的资源失败等等这些情形。

1.3异常有哪些类别?

java语言中有两种类别的异常,即已检查异常和未检查异常(Checkedand
Unchecked Exception)。

1.3.1已检查异常

已检查异常是编写良好的程序所能预料并且能从它恢复过来的这类异常。

例如程序要求输入文件名,但用户输入了一个不存在的文件的文件名时,程序利用java.io.FileReader构造文件将抛出java.io.FileNotFoundException,这时编写良好的程序应该从中恢复,提醒用户输入正确的文件名。
已检查异常是Throwable以及其不是RuntimeException和Error子类的类来表示的。

1.3.2未检查异常

未检查异常包括Error和RuntimeException两种。
1)error异常是由于程序外的因素导致的,例如资源耗尽的错误,这种错误不是程序所能预料到的,除了通告错误给用户,尽力使程序安全地终止外,程序再也无能为力。例如程序顺利打开一个文件来做读写,但是由于硬件错误,导致无法操作,此时将抛出java.io.IOError.Errors异常是由Error类及其子类来表示的。

2)Runtimeexceptions,是程序内部出错的情形,一般是程序不可预料和恢复的。通常这种异常是由于程序Bug引起的,例如程序逻辑错误或者使用API不当导致的。例如利用FileReader构造函数时传递一个空指针,那么这个构造函数将抛出一个NullPointerException异常。程序可以捕获这个异常,但是应该将重点放在消除代码Bug上。
Runtime exceptions是由RuntimeException及其子类表示的。
一条经验准则:如果程序出现RuntimeException,那么一定你写的代码有问题了。

1.3.3java语言异常类的继承关系

java语言异常类的继承关系树如下图所示,其中红色是已检查异常,绿色是未检查异常。
(来自http://www.programcreek.com/2009/02/diagram-for-hierarchy-of-exception-classes/)



1.3.4
已检查异常和未检查异常的区别

实际上这个已检查异常到底是被谁检查呢?
已检查异常是为了编译时检测,他们必须在try-catch-finally结构中被显式地捕获或者传递;而未检查异常则没有这个要求,他们不需要被捕获或者声明抛出异常,当然也可以显式地捕获这种异常,只是编译器未强制要求。
例如:

package com.learningjava;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* this program list checked and unchecked exception in java
* @author wangdq
*/
public class ExceptionDemo4 {
public static void main(String[] args) throws IOException {
FileInputStream in = null;
try {
int a = 5/0;//uncheck exception ArithmeticException
System.out.println(a);
in = new FileInputStream("1.txt");//checked exception FileNotFoundException
in.read();//checked exception IOException throw it
}
catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(in != null) {
in.close();
}
}
}
}


这里文件操作包括两个已检查异常,而除0操作则是从RuntimeException派生而来的未检查异常,
可以捕获也可以不捕获(最好的方式是避免这种错误,例如检测除数是否为0)。

1.3.5
什么时候使用已检查异常或者未检查异常?

决定这个问题的关键点在于:当这个异常发生时,客户端(使用异常类的代码)能够做些什么?

客户端对异常的举措

应该使用的类型

客户端不能采取什么有效措施(打印或者记录错误信息,不算在内)

让它成为未检查异常

客户端可以根据异常信息采取一些有效的恢复措施

  让它成为已检查异常

同时要避免将具体的已检查异常传递到高层,例如访问数据库的
SQLException
传递到业务层。可以将
SQLException
转化为未检查异常来完成这一转换,当然可以使用
initCause()
来保存原始异常的引用(见下文)。


2.异常的处理

2.1异常处理的任务是什么?交给谁处理?

异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误的处理器。

当程序中某个方法产生错误时,这个方法将会产生一个异常对象(exceptionobject,包括了错误类型、方法、行号等信息),并将其交付给java运行时系统。创建一个异常对象并将其交付给运行时系统,这个过程也叫做抛出异常(throwingan exception)。
那么这个异常到底交付给谁处理呢?
java中方法调用时,会产生一个调用栈(callstack),如下图所示:
(来自http://docs.oracle.com/javase/tutorial/essential/exceptions/definition.html)



运行时系统在调用栈中搜索包含处理异常代码块的方法,这个代码块被称作异常处理器(exceptionhandler)。
这个搜索u过程恰好跟调方法调用时的顺序相反,可称为逆向搜索。例如调用栈中方法调用顺序为A->B->C,异常是由方法C引起的,则处理器搜索的顺序就是C->B->A。

当找到合适的处理器(当方法抛出的异常类型和处理器能够处理的异常类型相匹配时,成为合适的处理器)时,运行时系统将异常对象传递给它。选择处理器的过程被称为异常的捕获(catchthe exception)。如果遍历栈后找不到合适的处理器,则运行时系统终止,意味着程序将结束。

2.2决定自己处理(catch)还是声明异常(declare)?

合法的java程序必须遵循java异常捕获或者异常说明(Catch or Specify Requirement)的规范。可能抛出异常的代码必须是下列两种处理方式之一:
1)在方法的try块中可能会抛出异常,但由该方法自己在catch块中提供处理器
2)通过在方法签名中利用throws关键字,声明异常,将异常传递给调用者,自己可以不用处理。

2.2.1 声明异常

通常,如果方法自己不能很好的处理异常时,声明异常能够给方法调用者处理这个异常的机会,而且调用者可能有更好的处理方式。
例如:
public FileInputStream(File file) throws FileNotFoundException;
在文件操作API中声明异常,那么调用者方法可以捕获这一异常,然后利用与GUI界面相关的方式来提醒用户,这比在文件操作API中弹出一个显示错误的对话框要更加合理。

在编写方法时,不必将所有可能抛出的异常都进行声明。

《java核心技术》一书指出,遇到下面两种情况时应该利用throws声明异常:
1)调用一个抛出已检查异常的方法,例如,FileInputStream构造器。

2)程序运行过程中发现错误,并用throw语句再抛出一个已检查异常时。

注意:
1) 如果一个方法可能抛出多个已检查异常,则应该在方法签名部分列出所有异常。

2) 如果子类中覆盖了超类的一个方法,子类方法中声明的已检查异常不能超过超类的声明的异常范围,即子类方法中抛出的异常范围更加小或者根本不抛出任何异常。
例如:

classFileOperation {

publicvoidreadFile(String file) throwsFileNotFoundException{

}

}

classFileOperationExextendsFileOperation{

@Override

publicvoidreadFile(Stringfile)throwsIOException{

}

}


此时提示错误信息:
ExceptionIOException is not compatible with throws clause inFileOperation.readFile(String)
子类FileOperationEx声明的异常超出了父类的范围,出现错误。

小节:
1)一个方法必须声明所有可能抛出的已检查异常; 而未检查异常要么不可控制(Error),要么就应该尽力避免发生(RuntimeException)。

2)不应该声明从RuntimeException类继承的那些未检查异常。
例如ArrayIndexOutOfBoundsException从RuntimeException间接继承而来,那么ArrayIndexOutOfBoundsException异常就没有被强制性要求用throws声明。

3)除了声明异常,还可以捕获异常,这样就可以不用将异常抛出到方法之外。(见下文)。

2.2.2 如何抛出异常?

抛出异常使用关键字throw(注意声明用throws,抛出用throw),只要找到一个合适的异常类,然后创建一个对象并用throw关键字抛出即可。

2.2.3 如何捕获异常?

捕获异常使用try-catch语句块。
eg:
try{
FileInputstreamin = new FileInputstream(filename);
}catch(IOexcpton e) {
dosomething
}

1)通常应该捕获那些我们知道怎么处理的异常,而将那些不知道怎么处理的异常传递出去而不是压制他们(surpress).
坚决反对使用如下处理:
catch(SomeExceptione){}

2)捕获多个异常
java7之前不或多个异常是这样的:

catch(IOException ex) {

logger.error(ex);

throw newMyException(ex.getMessage());

catch(SQLException ex) {

logger.error(ex);

throw newMyException(ex.getMessage());

}catch(Exception ex) {

logger.error(ex);

throw newMyException(ex.getMessage());

}


现在java7支持多个异常用'|'管道运算符连接起来如下:

catch(IOException| SQLException | Exception ex){

logger.error(ex);

throw newMyException(ex.getMessage());

}


3)异常的再次抛出与异常链

在catch语句中可以重新抛出一个与原始类型不同的异常,这样可以改变异常的类型。同时允许保留原始异常的引用,例如:
try{
dosome dtabase operation
}catch(SQLException e) {
Throwablese = new SerletException(“database error”);
se.initCause(e);
throwse;
}
当捕获到异常时使用se.getCause()方法得到原始异常。

4)非捕获异常的处理

java.lang.Thread.setDefaultUncaughtExceptionHandler()定义了一个线程的默认处理器,当由于没有捕获的异常导致程序突然终止且没有为该线程定义其他处理器时将会触发这个处理器。
使用默认处理器能在程序最终崩溃时给用户提供稍微友好的方式,例如迅雷、暴风影音等软件经常在崩溃时友好提示。
例如:

package com.learningjava;

import javax.swing.JLabel;
import javax.swing.JOptionPane;

/**
* this program show using of Thread.setDefaultUncaughtExceptionHandler
* @author wangdq
*
*/
public class ExceptionDemo3 {

public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler
(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
e.printStackTrace();//print stack info
//show a message dialog
JOptionPane.showMessageDialog(null,
new JLabel("<html><p><h1>哎呀,伦家崩溃啦...</h1>"
+ "</p><p><h2>很抱歉,你可以重启来解决你的问题.<h2></p>"
+ "<a href='#'>也可以发送反馈意见</a>"),
"系统提示",JOptionPane.WARNING_MESSAGE);
}
});
long[] l = new long[Integer.MAX_VALUE];
System.out.println(l.length);
}
}


运行效果如下所示:






2.3 保证任何情况下都能执行的语句块finally

finally语句块保证在任何情况下(即使发生异常时也一样)都被执行,通常用于释放文件资源、数据库链接等清除资源分配的工作。
注意:

1)java7中为释放资源引入了新的机制,即try-with-resources机制。该机制在try块中放入资源打开语句,不要要显式的书写close方法即可关闭资源,只要这个资源类实现了java.lang.AutoCloseable接口(该接口中包括了close方法),所有实现了java.io.Closeable类的对象都可以。例如:

try(FileInputStreamin = newFileInputStream("notExistFile.txt");

BufferedReaderreader =newBufferedReader(newInputStreamReader(in));) {

System.out.println(reader.readLine());

}catch(FileNotFoundException e) {

e.printStackTrace();

}catch(IOException e) {

e.printStackTrace();

}


2)finally中可能还会再次抛出异常,导致原始异常丢失,我们可以称为异常丢失或覆盖。
3)不要在finally块中书写return语句,新的版本中将会产生错误信息:
finallyblock does not complete normally。
在finally中书写return语句说明逻辑混乱,应该避免。

2.3 如何分析异常对象的信息 ?

当我们捕获到异常对象时,可以通过该对象的方法来获取信息,帮助程序员修改程序或者通知用户。Throwable类定义了几个有效的方法:
publicString getMessage();
publicString getLocalizedMessage() ;
publicsynchronized Throwable getCause() ;
publicvoid printStackTrace() ;
这些API用法可以参考javaAPI文档。其中用得较多的就是printStackTrace()方法,将栈的跟踪信息利用标准错误流打印出来。

4.自定义异常

当java内置的异常类不能很准确描述你的异常情形时,可以自己定义异常类来提供更合适的错误描述,以便调试。
自定义的异常类,可以从Exception或者从其子类派生。
注意,如果新建的异常类能够表达的信息不多时,不建议自定义异常类,例如这样一个类:
publicclass DuplicateUsernameException
extendsException {}
除了名字能有些许暗示外,其余部分毫无变化,可以通过使用:
thrownew Exception("Username already taken");
或者:thrownew RuntimeException("Username already taken");
来处理。

当然如果能表达更多合适的信息,则可以生成一个自定义的类,如下所示:
publicclass DuplicateUsernameException
extendsException {
publicDuplicateUsernameException
(Stringusername){....}
publicString requestedUsername(){...}
publicString[] availableNames(){...}
}

5.处理异常的最佳实践

1)区分已检查和未检查异常,正确地使用它们。记住区分已检查和未检查异常的关键点:客户端在发生这个异常后能够做些什么?

2)不要利用异常来做简单的测试,程序逻辑错误要在运行之前避免。

package com.learningjava;

/**
* this program try to illustrate that try-catch will consume extral time
* @author wangdq
*
*/
public class ExceptionDemo5 {
public static void main(String[] args) {

int a,b= 4,c=2;
final long startTime = System.nanoTime();
try {
a = b/c;
System.out.println(a);
} catch(ArithmeticException e) {

}
final long duration = System.nanoTime() - startTime;
//try-catch time consumend 1587423 nanoseconds
//without try-catch 912895 nanoseconds
System.out.println("try-cacth time consumed in nanoseconds: "+duration);

}
}


测试这段程序:

使用try-catch块执行时时间消耗1587423纳秒,是不使用try-catch时912895纳秒的1.7倍多。这说明,只在异常的情况下使用异常机制,否则要付出额外的代价。

3)捕获异常时尽量节省代码,不要导致代码量的急剧增加。
首先,不要过分的细化异常;其次可以用一个catch语句捕获多个异常。
例如:
try{
n = stack.pop;
}catch(EmptyStackException e) {
...
}
try{
out.writeInt(n);
}catch(IOExceptione) {…}

不要将每段代码都放在独立的try-catch语句之中,完全可以这样写:
try{
n= stack.pop;
out.writeInt(n);
}catch(EmptyStackException |IOException e) {…}

4)使用更加具体的异常类或者抛出更加适合的异常对象。
java类中Exception类见存在继承层次,我们应该抛出更实际运行代码出错更加密切的异常类,而不是抛出像Exception这样的顶层类,那样能获得的错误信息相当少,这也是为什么有那么多具体异常类的原因了。
同时,当一个异常对于客户端来说不能很好的解释时,应该将其转换为合适的异常。

5)在检测错误时,苛刻的抛出异常要比放任异常更好
当程序遇到错误时,例如遇到文件名参数为null时,是该返回null还是抛出异常呢?尽早抛出异常更好。在这里抛出异常,比后面出现难以理解的NullPointerException更好。

6)不要压制或者忽略异常。
如果异常在当前方法不能得到很好的处理,那么请不要羞于将异常传递给调用者,也许高层有更好的解释或者让高层通告用户发生了错误都是合适的。
不要书写这样的处理代码来忽略异常:
try{

..

}catch(SomeException ex){

//just empty

}
5)和6)和起来称为”早抛出,晚捕获”(Throw Early or Fail-Fast,Catch Late)。

7)自定义异常应该用得更加合理。
如果仅仅为了小的改进就派生自己的异常类,就显得毫无意义;仅当新定义的类能表达更多有价值的错误信息,更好地帮助程序恢复时才使用自定义异常类。合理地使用自定义异常类能给程序错误提供更多的信息。

8)始终记得关闭资源。
当异常发生时,程序被终止,我们应该在finally块或者使用try-with-resources机制来关闭相关的资源。

9)为异常类及抛出异常的方法书写文档,并且记录异常。
利用@throwsMyException将抛出的异常文档化。
同时当程序发生异常时,确保没一个异常只记录了一次,避免产生误解。

6.参考资料

[1]:
Best Practices for Exception Handling

 http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html?page=1

[2]:
Exceptions 

   http://docs.oracle.com/javase/tutorial/essential/exceptions/index.html
[3]:Checked
or Unchecked Exceptions?
   http://tutorials.jenkov.com/java-exception-handling/checked-or-unchecked-exceptions.html
[4]:Java Exception
Handling Tutorial with Examples and Best Practices
   http://www.javacodegeeks.com/2013/07/java-exception-handling-tutorial-with-examples-and-best-practices.html
[5]:Try with Resource
Example – Java 7 Feature for automatic resource management
http://www.journaldev.com/592/try-with-resource-example-java-7-feature-for-automatic-resource-management [6]Catching
Multiple Exceptions in single catch and Rethrowing Exceptions with Improved Type Checking – Java 7 Feature
http://www.journaldev.com/629/catching-multiple-exceptions-in-single-catch-and-rethrowing-exceptions-with-improved-type-checking-java-7-feature [7] :
Exception Hierarchy in Java – Diagram
http://www.programcreek.com/2009/02/diagram-for-hierarchy-of-exception-classes/
[8]:
《java核心技术:卷一》 机械工业出版社 第八版
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  异常处理 java