`
vboy1988
  • 浏览: 7773 次
  • 性别: Icon_minigender_1
  • 来自: 成都
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

(转)异常设计----何使用异常的原则

阅读更多

摘要
  本文是设计技术专栏文章,讨论有关异常设计的问题。本文关注何时使用异常,并举例演示异常的恰当使用。此外,本文还提供一些异常设计的基本原则。

  五个月前,我开始撰写有关设计对象的文章。本文是设计文技术系列文章的延续,讨论了有关错误报告和异常的设计原则。我假设读者已经知道什么是异常,以及异常是如何工作的。你若想回顾一下异常方面的知识,请阅读本文的姐妹篇《Java异常》。
   
异常的好处

         异常带来诸多好处。首先,它将错误处理代码从正常代码(normal code)中分离出来。你可以将那些执行概率为99.9%的代码封装在一个try块内,然后将异常处理代码----这些代码是不经常执行的----置于catch子句中。这种方式的好处是,正常代码因此而更简洁。
  如果你不知道如何处理某个方法中的一个特定错误,那么你可以在方法中抛出异常,将处理权交给其他人。如果你抛出一个检查异常(checked exception),那么Java编译器将强制客户程序员(cilent programmer)处理这个潜在异常,或者捕捉之,或者在方法的throws子句中声明之。Java编译器确保检查异常被处理,这使得Java程序更为健壮。
   
何时抛出异常

  异常应于何时抛出?答案归于一条原则:
  如果方法遇到一个不知道如何处理的意外情况(abnormal condition),那么它应该抛出异常。
  不幸的是,虽然这条原则易于记忆和引用,但是它并不十分清晰。实际上,它引出了另一个的问题:什么是意外情况?
  这是一个价值6.4万美元的问题。是否视某特殊事件为“意外情况”是一个主观决定。其依据通常并不明显。正因为如此,它才价值不菲。
  一个更有用的经验法则是:
  在有充足理由将某情况视为该方法的典型功能(typical functioning )部分时,避免使用异常。
  因此,意外情况就是指方法的“正常功能”(normal functioning)之外的情况。请允许我通过几个例子来说明问题。
   
几个例子

  第一个示例使用java.io包的FileInputStream类和DataInputStream类。这是使用FileInputStream类将文件内容发送到标准输出(standard output)的代码:
// In source packet in file except/ex9/Example9a.java
import java.io.*;
class Example9a {

    public static void main(String[] args)
        throws IOException {

        if (args.length == 0) {
            System.out.println("Must give filename as first arg.");
            return;
        }

        FileInputStream in;
        try {
            in = new FileInputStream(args[0]);
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't find file: " + args[0]);
            return;
        }

        int ch;
        while ((ch = in.read()) != -1) {
            System.out.print((char) ch);
        }
        System.out.println();

        in.close();
    }
}
  在本例中,FileInputStream类的read方法报告了“已到达文件末尾”的情况,但是,它并没有采用抛出异常的方式,而是返回了一个特殊值:-1。在这个方法中,到达文件末尾被视为方法的“正常”部分,这不是意外情况。读取字节流的通常方式是,继续往下读直到达字节流末尾。
  与此不同的是,DataInputStream类采取了另一种方式来报告文件末尾:
// In source packet in file except/ex9b/Example9b.java
import java.io.*;
class Example9b {

    public static void main(String[] args)
        throws IOException {

        if (args.length == 0) {
            System.out.println("Must give filename as first arg.");
            return;
        }

        FileInputStream fin;
        try {
            fin = new FileInputStream(args[0]);
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't find file: " + args[0]);
            return;
        }

        DataInputStream din = new DataInputStream(fin);
        try {
            int i;
            for (;;) {
                i = din.readInt();
                System.out.println(i);
            }
        }
        catch (EOFException e) {
        }

        fin.close();
    }
}
  DataInputStream类的readInt()方法每次读取四个字节,然后将其解释为一个int型数据。当读到文件末尾时,readInt()方法将抛出EOFException。
  这个方法抛出异常的原因有二。首先,readInt()无法返回一个特殊值来指示已经到达文件末尾,因为所有可能的返回值都是合法的整型数据。(例如,它不能采用-1这个特殊值来指示文件末尾,因为-1可能就是流中的正常数据。)其次,如果readInt()在文件末尾处只读到一个、两个、或者三个字节,那么,这就可以视为“意外情况”了。本来这个方法是要读四个字节的,但只有一到三个字节可读。由于该异常是使用这个类时的不可分割的部分,它被设计为检查型异常(Exception类的子类)。客户程序员被强制要求处理该异常。
  指示“已到达末尾”情况的第三种方式在StringTokenizer类和Stack类中得到演示:
// In source packet in file except/ex9b/Example9c.java
// This program prints the white-space separated tokens of an
// ASCII file in reverse order of their appearance in the file.
import java.io.*;
import java.util.*;
class Example9c {

    public static void main(String[] args)
        throws IOException {

        if (args.length == 0) {
            System.out.println("Must give filename as first arg.");
            return;
        }

        FileInputStream in = null;
        try {
            in = new FileInputStream(args[0]);
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't find file: " + args[0]);
            return;
        }

        // Read file into a StringBuffer
        StringBuffer buf = new StringBuffer();
        try {
            int ch;
            while ((ch = in.read()) != -1) {
                buf.append((char) ch);
            }
        }
        finally {
            in.close();
        }

        // Separate StringBuffer into tokens and
        // push each token into a Stack
        StringTokenizer tok = new StringTokenizer(buf.toString());
        Stack stack = new Stack();
        while (tok.hasMoreTokens()) {
            stack.push(tok.nextToken());
        }

        // Print out tokens in reverse order.
        while (!stack.empty()) {
            System.out.println((String) stack.pop());
        }
    }
}
  上面的程序逐字节读取文件,将字节数据转换为字符数据,然后将字符数据放到StringBuffer中。它使用StringTokenizer类提取以空白字符为分隔符的token(这里是一个字符串),每次提取一个并压入Stack中。最后,所有token都被从Stack中弹出并打印,每行打印一个。因为Stack类实现的是后进先出(LIFO)栈,所以,打印出来的数据顺序和文件中的数据顺序刚好相反。
  StringTokenizer类和Stack类都必须能够指示“已到达末尾”情况。StringTokenizer的构造方法接纳源字符串。每一次调用nextToken()方法都将返回一个字符串,它是源字符串的下一个token。源字符串的所有token都必然会被消耗掉,StringTokenizer类必须通过某种方式指示已经没有更多的token供返回了。这种情况下,本来是可以用一个特殊的值null来指示没有更多token的。但是,此类的设计者采用了另一个办法。他提供了一个额外的方法hasMoreTokens(),该方法返回一个布尔值来指示是否已到达末尾。每次调用nextToken()方法之前,你必须先调用hasMoreTokens()。
  这种方法表明设计者并不认为到达token流的末尾是意外情况。相反,它是使用这个类的常规情况。然而,如果你在调用nextToken()之前不检查hasMoreTokens(),那么你最后会得到一个异常NoSuchElementException。虽然该异常在到达token流末尾时抛出,但它却是一个非检查异常(RuntimeException的子类)。该异常的抛出不是为了指示“已到达末尾”,而是指示一个软件缺陷----你并没有正确地使用该类。
  与此类似,Stack类有一个类似的方法empty(),这个方法返回一个布尔值指示栈已经为空。每次调用pop()之前,你都必须先调用empty()方法。如果你忘了调用empty()方法,而直接在一个空栈上调用pop()方法,那么,你将得到一个异常EmptyStackException。虽然该异常是栈已经为空的情况下抛出的,但它也是一个非检查异常。它的作用不是检测空栈,而是指示客户代码中的一个软件缺陷(Stack类的不恰当使用)。
   
异常表示没有遵守契约

  通过上面的例子,你应该已经初步了解到,何时应抛出异常而不是使用其他方法进行通信。若从另一个角度来看待异常,视之为“没有遵守契约”,你可能对应当怎样使用异常有更深层的理解。
  面向对象程序设计中经常讨论的一个设计方法是契约设计,它指出方法是客户(方法的调用者)和声明方法的类之间的契约。这个契约包括客户必须满足的前置条件(precondition)和方法本身必须满足的后置条件(postcondition)。
  前置条件
  String类的charAt(int index)方法是一个带有前置条件的方法。这个方法规定客户传入的index参数的最小取值是0,最大取值是在该String对象上调用length()方法的结果减去1。也就是说,如果字符串长度为5,那么index参数的取值限于0、1、2、3、4。
  后置条件
  String类的charAt(int index)方法的后置条件要求返回值必须是该字符串对象在index位置上的字符数据,而且该字符串对象必须保持不变。
  如果客户调用charAt()并传入-1、和length()一样大或者更大的值,那就认为客户没有遵守契约。这种情况下,charAt()方法是不能正确执行的,它将抛出异常StringIndexOutOfBoundsException。该异常指出客户程序中存在某种缺陷或String类使用不当。
  如果charAt()方法接收的输入没有问题(客户遵守了契约),但是由于某种原因它无法返回指定的索引上的字符数据(没有满足后置条件),它将抛出异常来指示这种情况。这种异常指出方法的实现中包含缺陷或者方法在获得运行时资源上存在问题。
  因此,如果一个事件表示了“异常条件”或者“没有遵守契约”,那么,Java程序所要做的就是抛出异常。
   
抛出什么?

  一旦你决定抛出异常,你就要决定抛出什么异常。你可以抛出Throwable或其子类的对象。你可以抛出Java API中定义的、或者自定义的Throwable对象。那么,如何决定?
  
  通常,你只需要抛出异常,而非错误。Error是Throwable的子类,它用于指示灾难性的错误,比如OutOfMemoryError,这个错误将由JVM报告。有时一个错误也可以被Java API抛出,如java.awt.AWTError。然而,在你的代码中,你应该严格限制自己只抛出异常(Exception的子类)。把错误的抛出留给那些大牛人。
  检查型异常和非检查型异常
  现在,主要问题就是抛出检查型异常还是非检查型异常了。检查型异常是Exception的子类(或者Exception类本身),但不包括RuntimeException和它的子类。非检查型异常是RuntimeException和它的任何子类。Error类及其子类也是检查型的,但是你应该仅着眼于异常,你所做的应该是决定抛出RuntimeException的子类(非检查异常)还是Exception的子类(检查异常)。
  如果抛出了检查型异常(而没有捕获它),那么你需要在方法的throws子句中声明该异常。客户程序员使用这个方法,他要么在其方法内捕获并处理这个异常,要么还在throws子句中抛出。检查型异常强制客户程序员对可能抛出的异常采取措施。
  如果你抛出的是非检查型异常,那么客户程序员可以决定捕获与否。然而,编译器并不强制客户程序员对非检查型异常采取措施。事实上,他们甚至不知道可能这些异常。显然,在非检查型异常上客户程序员会少费些脑筋。
  有一个简单的原则是:
  如果希望客户程序员有意识地采取措施,那么抛出检查型异常。

  一般而言,表示类的误用的异常应该是非检查型异常。String类的chartAt()方法抛出的StringIndexOutOfBoundsException就是一个非检查型异常。String类的设计者并不打算强制客户程序员每次调用charAt(int index)时都检查index参数的合法性。
  另一方面,java.io.FileInputStream类的read()方法抛出的是IOException,这是一个检查异常。这个异常表明尝试读取文件时出错了。这并不意味着客户程序员错误地使用了FileInputStream类,而是说这个方法无法履行它地职责,即从文件中读出下一个字节。FileInputStream类地设计者认为这个意外情况很普遍,也很重要,因而强制客户程序员处理之。
  这就是窍门所在。如果意外情况是方法无法履行职责,而你又认为它很普遍或很重要,客户程序员必须采取措施,那么抛出检查型异常。否则,抛出非检查型异常。
  自定义异常类
  最后,你决定实例化一个异常类,然后抛出这个异常类的实例。这里没有具体的规则。不要抛出用一条字符串信息指出意外情况的Exception类,而是自定义一个异常类或者从已有异常类中选出一个合适的。那么,客户程序员就可以分别为不同的异常定义相应的catch语句,或者只捕获一部分。
  你可能希望在异常对象中嵌入一些信息,从而告诉catch子句该异常的更详细信息。但是,你并不仅仅依赖嵌入的信息来区别不同的异常。例如,你并不希望客户程序员查询异常对象来决定问题发生在I/O上还是非法参数。
  注意,String.charAt(int index)接收一个非法输入时,它抛出的不是RuntimeException,甚至也不是IllegalArgumentException,而是StringIndexOutOfBoundsException。这个类型名指出问题来自字符串索引,而且这个非法索引可以通过查询这个异常对象而找出。
   
结论

  本文的要点是,异常就是意外情况,而不该用于报告那些可以作为方法的正常功能的情况。虽然使用异常可以分离常规代码和错误处理代码,从而提高代码的可读性,但是,异常的不恰当使用会降低代码的可读性。
  
以下是本文提出的异常设计原则:
     如果方法遭遇了一个无法处理的意外情况,那么抛出一个异常。
      避免使用异常来指出可以视为方法的常用功能的情况。
       如果发现客户违反了契约(例如,传入非法输入参数),那么抛出非检查型异常。
       如果方法无法履型契约,那么抛出检查型异常,也可以抛出非检查型异常。
       如果你认为客户程序员需要有意识地采取措施,那么抛出检查型异常。

  关于作者 

  Bill Venners拥有长达12年的软件从业经验。他以Artima软件公司的名义在硅谷提供软件咨询和培训服务。他精通不同平台上的多种语言,包括针对微处理器的汇编程序设计、Unix上的C编程、Windows上的C++编程、和Web上的Java开发,所开发的软件覆盖了电子、教育、半导体和人身保险等行业。他是《深入Java虚拟机》的作者。

Resources

Recommended books on Java Design 
http://www.artima.com/designtechniques/booklist.html

Source packet that contains the example code used in this article 
http://www.artima.com/flexiblejava/exceptions.html

The discussion forum devoted to the material presented in this article 
http://www.artima.com/flexiblejava/fjf/exceptions/index.html

Object Orientation FAQ 
http://www.cyberdyne-object-sys.com/oofaq/

7237 Links on Object Orientation 
http://www.rhein-neckar.de/~cetus/software.html

The Object-Oriented Page 
http://www.well.com/user/ritchie/oo.html

Collection of information on OO approach 
http://arkhp1.kek.jp:80/managers/computing/activities/OO_CollectInfor/OO_CollectInfo.html

Design Patterns Home Page 
http://hillside.net/patterns/patterns.html

A Comparison of OOA and OOD Methods 
http://www.iconcomp.com/papers/comp/comp_1.html

Object-Oriented Analysis and Design Methods: A Comparative Review 
http://wwwis.cs.utwente.nl:8080/dmrg/OODOC/oodoc/oo.html

Patterns discussion FAQ 
http://gee.cs.oswego.edu/dl/pd-FAQ/pd-FAQ.html

Implementing Basic Design Patterns in Java (Doug Lea) 
http://g.oswego.edu/dl/pats/ifc.html

Patterns in Java AWT 
http://mordor.cs.hut.fi/tik-76.278/group6/awtpat.html

Software Technology's Design Patterns Page 
http://www.sw-technologies.com/dpattern/


Previous Design Techniques articles

"Object finalization and cleanup" -- How to design classes for proper object cleanup. http://www.javaworld.com/jw-06-1998/jw-06-techniques.html 
"What's a method to do?" -- How to maximize cohesion while avoiding explosion. http://www.javaworld.com/jw-05-1998/jw-05-techniques.html 
"Designing fields and methods" -- How to keep fields focused and methods decoupled. http://www.javaworld.com/jw-04-1998/jw-04-techniques.html 
"Designing object initialization" -- Ensure proper initialization of your objects at all times. http://www.javaworld.com/jw-03-1998/jw-03-techniques.html 
quot;Introduction to "Design Techniques"" -- A look at the role of design in the context of the overall software development process. http://www.javaworld.com/jw-02-1998/jw-02-techniques.html 

 

原文地址:http://www.cnblogs.com/JavaVillage/articles/384483.html

分享到:
评论

相关推荐

    面向对象技术与UML课件及源代码-by 南邮-陈杨

    本书为中南大学精品教材立项项目,分为上下两篇共21章,涵盖了面向对象技术中Java开发环境配置、程序设计基础、面向对象原理以及UML的知识。本书使用的开发环境是JDK 1.6+Eclipse 3.3+Rational Rose 2003,逐步引领...

    C++MFC教程

    例如当菜单转中之后会有WM_COMMAND消息发送,WPARAM的高字中(HIWORD(wParam))是命令的ID号,对菜单来讲就是菜单ID。当然用户也可以定义自己的消息名称,也可以利用自定义消息来发送通知和传送数据。 2、谁将收到...

    java开源包1

    同时,任何第三方都可以使用OAUTH认证服务,任 何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP,JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间...

    语言程序设计课后习题答案

    使用二进制有何优点和缺点? 解: 二进制是基数为2,每位的权是以2 为底的幂的进制,遵循逢二进一原则,基本符号为0和1。采用二进制码表示信息,有如下几个优点:1.易于物理实现;2.二进制数运算简单;3.机器可靠性...

    java开源包10

    同时,任何第三方都可以使用OAUTH认证服务,任 何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP,JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间...

    机房UPS设计方案.doc

    UPS》YD/T 1095-2000国家信息产业部 2、《信息技术设备不间断电源通用技术条件》GB/T 14715 3、《不间断电源设备》GB/T7260-1987 4、《通信电源设备安装设计规范》YD5040-97 2.3、设计原则 1、高可靠性: 这是...

    JAVA入门1.2.3:一个老鸟的JAVA学习心得 PART1(共3个)

    7.5.6 重载中的最难点——参数匹配原则 171 7.6 使用类的实例作为方法参数 172 7.6.1 超车方法:使用类实例做参数 172 7.6.2 调用这个方法 173 7.6.3 发生了什么 174 7.7 加餐:局部变量和实例变量 175 7.7.1 ...

    Java入门1·2·3:一个老鸟的Java学习心得.PART3(共3个)

    7.5.6 重载中的最难点——参数匹配原则 171 7.6 使用类的实例作为方法参数 172 7.6.1 超车方法:使用类实例做参数 172 7.6.2 调用这个方法 173 7.6.3 发生了什么 174 7.7 加餐:局部变量和实例变量 175 7.7.1 ...

    网络设计方案(3).doc

    XX校园网络设计方案书 第四组: 组员:厉健、 杨锋、 刘永海、 吴霞、 陈伟狄 、朱刚、何臻伟 汇报人:朱刚 目 录 第一章 建设目标与原则 3 1.1 XX学院网络建设目标 3 1.2 校园网设计原则 4 第二章 校园网建设方案 6...

    uboott移植实验手册及技术文档

    本文件中的 nand_init()函数,本例使用后者。fs2410.c代码如下: #if defined(CONFIG_CMD_NAND) typedef enum { NFCE_LOW, NFCE_HIGH } NFCE_STATE; static inline void NF_Conf(u16 conf) { S3C2410_...

    java开源包8

    同时,任何第三方都可以使用OAUTH认证服务,任 何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP,JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间...

    重建——改善既有代码的设计

    本书清晰地揭示了重构的过程,解释了重构的原理和最佳实践方式,并给出了何时以及何地应该开始挖掘代码以求改善。 章节列表如下: 目录 第1章 重构,第一个案例1 1.1 起点1 1.2 重构的第一步7 1.3 分解并重组...

    程序文件生产过程控制程序(1).docx

    交货准时率 生产计划达成率 物料报废率 交货准时率 生产计划达成率 物料报废率 用何指标衡量用何程序、方法 生产计划控制程序 生产过程控制程序 用何指标衡量 用何程序、方法 生产计划控制程序 生产过程控制程序 ...

    程序文件生产过程控制程序.docx

    交货准时率 生产计划达成率 物料报废率 交货准时率 生产计划达成率 物料报废率 用何指标衡量用何程序、方法 生产计划控制程序 生产过程控制程序 用何指标衡量 用何程序、方法 生产计划控制程序 生产过程控制程序 ...

    JAVA上百实例源码以及开源项目

    例如,容易实现协议的设计。 Java EJB中有、无状态SessionBean的两个例子 两个例子,无状态SessionBean可会话Bean必须实现SessionBean,获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,计算利息等;...

    JAVA上百实例源码以及开源项目源代码

    例如,容易实现协议的设计。 Java EJB中有、无状态SessionBean的两个例子 两个例子,无状态SessionBean可会话Bean必须实现SessionBean,获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,计算利息等;在...

    java开源包11

    同时,任何第三方都可以使用OAUTH认证服务,任 何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP,JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间...

    java开源包2

    同时,任何第三方都可以使用OAUTH认证服务,任 何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP,JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间...

    java开源包3

    同时,任何第三方都可以使用OAUTH认证服务,任 何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP,JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间...

    java开源包6

    同时,任何第三方都可以使用OAUTH认证服务,任 何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP,JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间...

Global site tag (gtag.js) - Google Analytics