Nomad & His Life

博观而约取,厚积而薄发
posts - 15, comments - 88, trackbacks - 0, articles - 0
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

《Robust Java》读书笔记

Posted on 2006-03-28 02:26 Nomad 阅读(1200) 评论(0)  编辑  收藏 所属分类: Java

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
用于准确格式控制。


只有注册用户登录后才能发表评论。


网站导航: