《
Robust Java
》
作者:
Stephen Stelting
本书详细的讲述了异常的各个方面的知识,兼带介绍了测试和调试的基本方法。是了解
Java
异常的极佳教材。
异常概念
从某种意义上讲,异常就是这样一种消息:它传送一些系统问题、故障及未按规定执行的动作的相关信息。异常包含信息,以将信息从应用程序的一部分发送到另一部分。
异常本身表示消息,指发送者传给接受者的数据
“
负荷
”
。首先,异常基于类的类型来传输有用信息。很多情况下,只需异常对象的类即能识别故障本因并更正问题。其次,异常还带有可能有用的数据(如属性)。异常消息或故障本质原因等数据属于这个范畴。
在处理异常时,消息必须有接受者;否则将无法处理产生异常的底层问题。
异常类层次结构
所有异常有一个共同祖先
Throwable
(可抛出)。
Throwable
指定代码中可用异常传播机制通过
Java
应用程序传输的任何问题的共性。
Throwable
有两个重要的子类:
Exception
(异常)和
Error
(错误),二者都是
Java
异常处理的重要子类,各自都包含大量子类。
“
异常
”
是应用程序中可能的可预测、可恢复的问题。
Java API
文档记录给出的定义是:
“
合理应用程序可能需要捕获的情况。
”
一般地,大多数异常表示轻度到中度的问题。
异常一般在特定环境下产生的,通常出现于代码的特定方法或操作中。
“
错误
”
表示运行应用程序中的较严重问题。
Java API
文档记录给出的定义是
:“
是
Throwable
的一个子类,代表严重问题,合理应用程序不应试图捕获它。大多数此类错误属反常情况。
”
大多数错误与代码编写者执行的操作无关,而表示代码运行时
JVM
(
Java
虚拟机)出现的问题。例如,当
JVM
不再有继续执行操作所需的内存资源时,将出现
OutOfMemoryError
。
Exception
有一个重要的子类
RuntimeException
。
RuntimeException
及其子类表示
“JVM
常用操作
”
引发的错误。例如,若试图使用空值
对象引用、除数为零,或数组越界,则将分别引发运行时异常(
NullPointerException
、
ArithmeticException
)和
ArrayIndexOutOfBoundException
。
异常的处理或声明选项
在处理潜在故障时,有两个选项。在调用可能引发异常的方法时,可以捕获异常,也可以声明该方法抛出异常。也就是说的
“
处理(
handle
)或声明(
declare
)
”
规则。
处理异常:
try
、
catch
和
finally
若要捕获异常,则必须在代码中添加异常处理器块。
import java.io.*;
public class EchoInputTryCatchFinally{
public static void main(String [] args) {
System.out.println("Enter text to echo");
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader inputReader = new BufferedReader(isr);
try{
String inputLine = inputReader.readLine();
System.out.println("READ: "+inputLine);
}
catch(IOException exc){
System.out.println("Exceptin encountered: "+exc);
}
finally{
System.out.println("My job here is done.");
}
}
}
1.try
块
在将一个或多个语句放入
try
块时,则表示这些语句可能抛出异常。编译器知道可能要发生异常,于是用一个特殊结构评估块内所有语句。
2.catch
块
当
问题出现时,一种选择是定义代码块来处理问题,
catch
的目的便在与此。
catch
块是
try
块所产生异常的接受者。基本原理为:一旦生成异常,则
try
块的执行中止,
JVM
将查找相应的
catch
块。在
try-catch
结构中,可能有多个
catch
块;此时,异常将交由第一个匹配的
catch
块
处理。
3.finally
块
还可以定义这样
一个代码块,无论试图运行
try
块代码的结果如何,该块一定运行。
finally
块作用便在于此。在常见的所有环境中,
finally
块都将运行。无论
try
块是否运行完,无论是否产生异常,也不论异常是否在
catch
块得到处理,
finally
块都将运行。
try-catch-finally
的规则
·
必须在
try
之后添加
catch
块。可任意接不接
finally
块。
·
必须遵守块顺序:如代码同时使用
catch
和
finally
块,则必须将
catch
块放在
try
块之后。
· try
块与相应的
catch
或
finally
之间可能不存在语句。
· catch
块与特定异常类的类型相关。
·
一个
try
块可能有多个
catch
块。若如此,将执行第一个匹配块。有一个经验法则:要按从最具体到最一般的顺序组织处理块。
·
除下列情况,总将执行
finally
作为结束:
a.JVM
过早中止(调用
System.exit(int)
)
b.
在
finally
块中抛出一个未处理的异常。
c.
计算机断电、失火,或遭遇病毒攻击。
·
可嵌套
try-catch-finally
结构
·
在
try-catch-finally
结构中,可重新抛出异常。
声明异常
若要声明异常,则必须将其添加到方法签名块的结束位置,即输入部分之后。以下是一个异常声明方法的示例:
public void errorProneMethod(int input) throws java.io.IOException {
...
}
这样,声明的异常将传给方法调用者,而且也通知了编译器;该方法的任何调用者必须遵守处理或声明规则。
声明异常的规则
·
必须声明方法可能抛出的任何可检测异常(
checked exception
)。
·
非检测异常(
unchecked exception
)不是必须的,可声明,也可不声明。
·
调用方法必须遵守任何可检测异常的处理或声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明任何异常必须被覆盖方法所声明异常的同类或子类。
可检测异常和非可检测异常
Java
的可检测异常和非检测异常泾渭分明。可检测异常经编译器验证,对于声明抛出异常的任何方法,编译器将强制执行处理或声明规则。
非检测异常不遵循处理或声明规则。在产生此类异常时,不一定非要采取任何适当操作,编译器不会检查是否已解决这样一个异常。有两个主要类定义非检测异常:
RuntimeException
和
Error
。
为
什么
Error
子类属于非检测异常?这是因为无法预知它们的产生时间。若
Java
应用程序内存不足,则随时可能出现
OutOfMemoryError
;起
因一般不是应用程序中的特殊调用,而是
JVM
自身的问题。另外,
Error
类一般表示应用程序无法解决的严重问题,故将这些类视为非检测异常。
RuntimeException
类也属于非检测异常,一个原因是普通
JVM
操作引发的运行时异常随时可能发生。与
Error
不同,此类异常一般有特定操作引发,但这些操作在
Java
应用
程序会频繁出现。另一个原因是:它们表示的问题不一定作为异常处理。可以在
try-catch
结构中处理
NullPointerException
,但若
在使用引用前测试空值,则更简单经济。
异常处理技术和实践
选择处理或声明
一个经验法则是:
“
尽可能去处理异常,如果没有能力处理就声明异常。
”
从本质上将,仅当方法缺少自我处理异常的信息、上下文或资源时,才将异常信息传给调用者。
标准异常处理选项
对于处理器代码块捕获的大多数错误情况,一般可以采用以下
9
种响应方式:
·
记录异常和相关信息
·
要求用户或应用程序输入信息
·
使用默认值或替换数据
·
将控制转移到应用程序的其他部分
·
将异常转换为其他形式
·
忽略问题
·
重试操作
·
采取替换或恢复操作
·
使系统作好停止准备
记录异常和相关信息
为有效处理异常,日志记录(
logging
)是最有效的工具之一。这有利于系统的长期开发和维护,有助于完成系统恢复、测试和调试等任务。一般要记录信息,可使用下列方法:
·
标准输出或标准错误流
·
自定义记录类
· Java
记录
API
1.
标准输出或标准错误流
对于简单的记录任务,可使用标准输出或错误流:
System.out
、
System.err
。
System.out.println("User error: replace user and continue");
System.err.println("Press any key. Go on. I dare ya.");
因为
System
类允许通过方法调用
System.setOut(PrintStream)
和
System.setErr(PrintStream),
将输出重定向到另一个目标位置,所以能够满足基本文件输出等的需要。
此类功能一般适用于简单应用程序的本地记录。而对于较复杂的应用程序,使用这些功能工作量过大。此外,一些
Java
代码类型运行在服务器的
JVM
中,不允许将输出定向到这些位置。
2.
自定义记录类
一些应用程序要求记录更灵活,更易配置。此时,应为系统开发记录资源(即开发一个类)来接受应用程序范围内的调用,并将它们记录到一个中心位置。这样的类常实现为静态资源或单模式(
Singleton
),以确保通用于系统,而不需要其他对象包含其引用。
import java.io.*;
import java.util.*;
public class CustomLogger {
private static final String DEFAULT_FILE="exceptions.log";
private static final String FILE_KEY="application.logfile";
private static CustomLogger instance = new CustomLogger();
private static PrintWriter outputLog;
private CustomLogger() {
String filename = System.getProperty(FILE_KEY,DEFAULT_FILE);
try{
outputLog = new PrintWriter(new FileWriter(filename,true));
}
catch (IOException exc) {
exc.printStackTrace();
}
}
public static CustomLogger getInstance() {
return instance;
}
public void log(String message) {
logMessage(new Date() + " " + message);
}
public void log(Throwable error) {
StringBuffer message = new StringBuffer(new Date() + " ERROR: "+
error.getClass().getName()+ System.getProperty("line.separator"));
message.append(error);
logMessage(message.toString());
}
private void logMessage(String message) {
outputLog.println(message);
outputLog.flush();
}
}
3.Java
记录
API
在
JDK1.4
中,
Sun
引入了记录
API
,以更灵活有效地记录错误、消息和通知。一般地,使用
java.util.logging
包中的几个类即可执行简单的记录任务。例如,若要将消息发送到标准输出,则代码如下所示:
Logger logException = Logger.getLogger("basic.exception.example");
logExcetption.log(Level.WARNING, "Unable to find configuration file.");
也可以方便地设置易于阅读的日志文件:
Logger logException = Logger.getLogger("exception.example");
try{
Handler fileOut = new FileHandler("exc.err",true);
fileOut.setFormatter(new SimpleFormatter());
logException.addHadndler(fileOut);
}
catch (IOException ex) {
//....
}
logException.log(Level.WARNING,"Exception generated");
要求用户或应用程序输入信息
对
于与最终用户
“
接近
”
的问题,最好让用户来确定响应应用程序相应问题的方式。这一般通过
GUI
管理,使用户作出选择或输入信息。对于简单故障,用户可能仅
需确定适当的操作序列;而对于较复杂的问题,则可能要添加附加或替换信息。很多
GUI
使用对话框来提示用户作出决策,在
AWT
中,可使用
java.awt.Dialog
类;在
Swing API
中,一般使用
javax.swing.JDialog
或
javax.swing.JOptionPane
。下面的示例代码显示了在出现错误时如何要
求用户输入信息:
private void showConfigErrorMessage() {
String msg = "Unable to load user preferences file: create new file?";
String title = "Application Error";
int result = JOptionPane.showConfirmDialog(null,msg,title,JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
if(result == JOptionPane.YES_OPTION) {
creatNewConfigFile();
}
else{
loadDefaultSettings();
}
}
使用默认值或替换数据
如
果使用替换或默认值,那么,即使在执行操作时遇到异常,程序仍可继续运行。在通信示例中。另一台服务器(或同一台服务器上的不同端口)可能是一个合适的替
换方案。若有可识别的任何标准值,则可以在应用程序中包含默认值。在最简单的情况下,可以将这些值作为应用程序中的常量(静态最终值):
private static final String ALTERNATE_HOST = "denver";
private static final int ALTERNATE_PORT = 5280;
更复杂一些的选项是允许默认值存储在配置文件中,并在运行时加载到应用程序。
private static final String ALTERNATE_HOST = "denver";
private static final String HOST_KEY = "client.app.host";
public void sendData(String host,int port,Serializable information, String propertyFile){
if(communication ==null){
communication = new ClientNetWork();
}
try{
communication.connect(host,port);
conmunication.sendData(information);
}
catch( IOException exc){
Properties serverProps = new Properties();
try{
FileInputStream input = FileInputStream(propertyFile);
serverProps.load(input);
String serverName = serverProps.getProperty(HOST_KEY,ALTERNATE_HOST);
sendData(serverName,port,information);
}
catch(IOException exc2) {
exc.printStackTrace();
}
}
}
将控制转移到应用程序的其他部分
还有一种将异常处理职责传给系统另一部分的非终极方式:不是从异常产生方法完全委托,而是开发一个异常处理类,使用它来集中处理应用程序代码。
private void saveOrderInfo(OrderInfo info){
try{
OrderService.getInstance().saveOrderInfoToFile(info);
}
catch(IOException exc)
{
handleFileSaveError(info);
}
}
将异常转换为其他形式
有
时,最好将异常转化为其他形式,以将异常更改为对应用程序更有意义的形式,提供更适当的上下文以准确描述错误。由于可将原始异常包装到另一个异常中(从
JDK1.4
开始),故不会失去原始异常的上下文或数据。下例显示解决
saveOrderInfo
方法产生的
IOException
的另一种方法:
catch
块将异常转化为
OrderException
抛给方法调用者。
private void saveOrderInfo(OrderInfo info) throws OrderException{
try {
OrderService.getInstance().saveOrderInfoToFile(info);
}
catch(java.io.IOException exc) {
throw new OrderException("File I/O error when saving ORderInfo",exc);
}
}
忽略问题
注
意,不能滥用这种方法,否则,代码将出现严重问题。确实存在这样的情况,即一个异常对应用程序的所有其他部分没有任何影响。例如,在调用
I/O
流的关闭方
法时产生的异常便属于这种类型。该方法可抛出表示问题的
IOException
,但应用程序除了记录事件之外,一般什么也不做。
//Declare member variable sourceFile in the class
private FileReader souceFile;
//...
try{
sourceFile.close();
}
catch (java.io.IOException exc) {
//...
}
重试操作
有些情况下,最适当的操作是在等一段时间后重试操作。在试图连接到服务器或数据库时,有时会发生异常,因为服务器的信息量过大。此时,最好再等一段时间,再尝试连接。
try{
clientNetwork.connect("www.talk-about-tech.com",5280);
}
catch (java.io.IOException exc)
{
try {
Thread.sleep(10000);
}
catch (InterruptedException exc2) {}
try{
clientNetwork.connect("www.talk-about-tech.com",5280);
}
catch (java.io.IOException exc)
{
//...
}
}
采取替换或恢复操作
有时,可采取补偿方式来处理异常。例如,若不能连接服务器,则可以在本地缓存数据,并在之后某一段时间将数据发送到服务器。理想情况下,恢复操作将允许应用程序正常运行。
try{
svr.connect("www.talk-about-tech.ocm",5280);
}
catch (IOException exc)
{
enableFileCache();
}
使系统作好停止准备
如果某一异常确实是应用程序的关键错误,则需要采取步骤,使系统作好停止准备。这种情况下,要确保该应用程序不致使系统的其他部分出现故障,也不会使数据处于不一致状态,这就需要完成以下几件事情:
·
若有打开的文件,则关闭它
·
若有基于连接的资源,则采用普通方式关闭
·
若有需要保持一致的信息,则一定要保存
·
根据需要,通知应用程序、客户端或子系统应用程序将结束。
处理异常时提倡的事情
尽可能地处理异常
要尽量处理处理异常,如果条件确实不允许,无法在自己的代码中完成处理,就考虑声明异常。如果人为避免在代码中处理异常,仅做声明,则是一种错误和懒惰的实践。
具体问题具体解决
异常的部分优点在于能为不同类型的问题提供不同的处理操作。有效异常处理的关键是识别特定故障场景,并开发解决此场景的特定相应行为。为了充分利用异常处理能力,需要为特定类型的问题构建特定的处理器块。
记录可能影响应用程序运行的异常
至少要采取一些永久方式,记录下可能影响应用程操作的异常。理想情况下,当然是在第一时间解决引发异常的基本问题。不过,无论采用什么处理操作,一般总应记录下潜在的关键问题。别看这个操作非常简单,但可以帮助您用很少的时间来跟踪应用程序复杂的问题起因。
根据情况将异常转换为业务上下文
构建异常成本不高,所以,若要通知一个应用程序特有的问题,有必要将应用程序转换为不同形式。若用业务特定状态表示异常,则代码更易维护。
J2SE 1.4
的异常有所增强,允许包装原始异常,故不会失去问题的原始上下文。
处理异常是忌讳的事项
一般不要忽略异常
在异常处理块中,一项最危险的举动是
“
不加通告
”
地处理异常。如下所示:
try {
Class.forName("business.domain.Customer");
}
catch (ClassNotFoundException exc) {}
经常能够在代码中看到类似的代码块。有人总喜欢在编写代码简单快速地编写空处理块,并
“
自我安慰地
”
宣称准备在
“
后期
”
添加恢复代码,但这个
“
后期
”
最终成了
“
无期
”
。
这种做法有什么坏处?如果异常对应用程序的其他部分确实没有任何负面影响,这未尝不可。但事实往往并非如此,异常会扰乱应用程序的状态;此时,这样的代码块无异于掩耳盗铃。
不要使用覆盖式异常处理块
另一个危险的处理实践式覆盖式处理器(
blanket handler
)。该代码的基本结构如下所示:
try{
//...
}
catch (Exception e) {
//...
}
使用覆盖式异常处理块有以下两个前提之一:
·
代码中只有一类问题。
这可能正确,但即便如此,也不应该使用覆盖式处理,捕获更具体的异常形式有利无弊。
·
单个恢复操作始终适用。
这几乎绝对错误。几乎没有哪个方法能放之四海而皆准,能应付出现的任何问题。
分析一下这样编写代码将发生的情况。只要方法不断抛出预期的异常集,则一切正常。但是,如果方法抛出了未预料到的异常,则无法看到要采取的操作。当覆盖式处理器对新异常类型执行千篇一律的任务时,只能间接看到异常处理结果。如果代码没有打印或记录语句,则根本看不到结果。
更糟的是,当代码发生变化时,覆盖式处理器将继续作用于所有新的异常类型,并以相同方式处理所有类型。一般地,在修改代码时,这是不智之举
——
要考虑方法更改所产生的影响,并相应更改异常处理块。
一般不要将特定异常转换为更通用的异常
将特定异常转换为更通用异常是一种错误做法。一般而言,这将取消异常起初抛出时产生的上下文,在将异常传到系统的其他位置时,将更难处理。见下例:
try{
//error-prone code
}
catch (IOException e) {
String msg = "If you didn't have a problem before, you do now!";
throw new Exception(msg);
}
因
为没有原始异常的信息,所以处理器块无法确定问题的起因,也不知如何更正问题。更糟的是,该方法必须声明它抛出
Exception
,实际上,这形同虚设,
对该方法的
Java
文档读者没有任何帮助作用。对于抛出通用
Exception
对象的方法,应执行哪种恢复操作?一般而言,若要重新抛出异常,要有
3
个理
由:
·
重新抛出同一异常,以采取一些操作,然后传送异常作进一步处理。
·
包装或重新抛出更具体的异常类型,以缩小后续处理器的问题类型。
·
包装或重新抛出不同类型的异常,转换为适当上下文,提供给不同应用程序子系统的处理器。在转换为业务异常时,一般会这样做。
不要处理能构避免的异常
对
于有些异常类型,实际上根本不必处理。一般地,要秉持这样一个理念:为不可避免的错误使用异常。虽然可用异常来处理形形色色的各种问题,但不一定非要这么
做。处理或声明异常必然会影响代码的运行效率。也就是说,若能避免某些问题类型,则成本比使用异常来解决要低。诸如处理空指针等问题。
高级异常处理概念
自定义异常
使用子定义异常有什么好处呢?创建自定义异常是为了表示应用程序的一些错误类型,为代码可能发生的一个或多个问题提供新的含义。可以显示代码多个位置之间的错误的相似性,也可区分代码运行时可能出现的相似问题的一个或多个错误,或给出应用程序中一组错误的特定含义。
创建和使用自定义异常并不难。遵循以下
3
个步骤即可。
1.
定义异常类
一般要定义新类来表示自定义异常。多数情况下,只需创建已有异常类的子类。
public class CustomerExistsException extends Exception {
public CustomerExistException() { }
public CustomerExistException(String message) {
super(message);
}
}
至少要继承
Throwable
或
Throwable
的子类。经常要定义一个或多个构造函数,以在对想中存储错误消息。在继承任何异常时,将自动继承
Throwable
类的一些标准特性,如:
·
错误消息
·
栈跟踪
·
异常包装
若要在异常中添加附加信息,则可以为类添加一些变量和方法:
public class CustomerExistsException extends Exception
{
private String customerName;
public CustomerExistsException() {}
public CustomerExistsException(String message) {
super(message);
}
public CustomerExistsException(String message, String customer){
super(message);
customerName = customer;
}
public String getCustomerName() {
return customerName;
}
}
由本例可知,可修改
CustomerExistsException
类,以支持其他属性。例如,可将
customerName
字符串(引发异常的记录的客户名)与异常联系起来。
2.
声明方法抛出自定义异常
这实际上时
“
处理或声明
”
规则的
“
声明
”
部分。为了使用自定义异常,必须通知调用代码的类:要准备处理这个异常类型。为此,声明一个或多个方法抛出异常:
public void insertCustomer(Customer c) throws CustomerExistsException {
//...
}
3.
找到故障点,新建异常并加上关键子
throw
最后一步实际上是创建对象,并通过系统传送该对象。为此,需要了解代码将在方法的哪个位置出现故障。根据情况,可能要使用以下部分或所有条件,来指示代码中的故障点。
(1)
外部问题
·
应用程序中产生的异常
·
其他方法返回的故障代码
(2)
内部问题
·
应用程序状态不一致
·
应用程序中的处理问题
子类
在
构建子类时,可以覆盖父类的方法,此时,必须遵守一些严格的规则。必须复制方法的签名:方法名、输入和输出都要匹配。不能使覆盖的方法比父类更私有。还要
注意,方法不能抛出父类方法未声明的异常。也就是说,在覆盖方法时,可抛出与父类的方法相同的异常或异常的子集。如果在父类方法中未定义,则不能抛出不同
的异常。
接口和抽象类的异常声明
抽象方法只提供方法签名,不真正定义任何代码;接口实际上全部由抽象方法组成。为这类方法定义异常时,实际上是在设置一种期望,描述最终实现的方法可能出现的错误。
public interface CommunicationServer {
public void processClient() throws ServerConnectException;
public void listen();
}
Java
核心语言中的异常
基本数据类型的常见问题
1.
不允许在布尔和数值类型间转换
在
Java
中,布尔变量不能转换为其他任何基本数据类型。也就是说,不能声明这样一个方法,它返回整型,并将结果视为布尔值。
2.
数值数据类型的数据截断
在两种情况下,可能出现这个问题:收缩转换和数学运算。
强制转换:
int i = 2048;
byte b = (byte)i;
数学运算:
int i,j,k;
i = 10000;
j = 400000;
k = i * j;
3.
数值数据类型的精度丢失
例:
double c= 0.025;
double d= 21.003;
double e=c*d;
System.out.println("c ="+c+", d="+d);
System.out.println("c*d="+e);
在
本例中,输出的乘积是
0.5250750000000001
。这个问题出现的是舍入错误。可采用几种方案来消除这个问题。如果要在操作小数值时确保精度,
则使用
BigDecimal
类。如果仅要求格式符合输出要求,则可用
java.lang.format
包中的类来处理舍入。
4.
数学运算和
ArithmeticException
除零错误。
一般建议
·
预先了解应用程序的数据要求,并制定相应规划。选项有:
a.
使用
BigDecimal
或
BigInteger
。
b.
选择一种足够大、足够精确,可以满足需要的存储类型。
·
在使用
/
或%运算符时,检查操作数中的
0
值。
·
在显示值时,使用满足需要的策略。选项有:
a.
原始基本类型用于基本需要。
b. BigDecimal
或
BigInteger
用于扩展精度。
c. NumberFormat
用于准确格式控制。