`
cppmule
  • 浏览: 448178 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类

异常设计----何使用异常的原则

    博客分类:
  • java
 
阅读更多

异常设计----何使用异常的原则

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

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

分享到:
评论

相关推荐

    行业分类-外包设计-用于使用数据库验证包装中的物品的系统的介绍分析.rar

    5. **异常处理**:当验证失败时,系统应具备何种应对策略,例如警报机制、错误修复流程等。 6. **性能与效率**:在大规模应用中,系统需能快速处理大量验证请求,同时保持高准确性。 7. **外包设计流程**:从需求...

    ISO 13849-1-2023 机械安全控制系统的安全相关部件第1部分:设计的一般原则.rar

    《ISO 13849-1-2023:机械安全控制系统的设计原则》是国际标准化组织(ISO)发布的一项重要标准,旨在规范机械安全控制系统的安全相关部件设计的一般原则。该标准的更新版在2023年发布,为机械工程领域的安全设计...

    JAVA技术61条面向对象设计的经验原则.txt

    ### JAVA技术61条面向对象设计的经验原则 #### 原则一:避免冗余代码 - **描述**:在面向对象设计时,应避免重复的代码,这有助于提高代码的可维护性和可读性。 - **应用**:通过继承、封装等机制减少不必要的重复...

    面向对象设计原则和设计模式的概念

    这意味着在程序中使用基类的地方,应该能够透明地使用其子类的对象,且不会出现异常或错误的行为。这一原则确保了继承的正确使用,避免了由于不当的继承造成的类型安全问题和运行时错误。例如,如果设计了一个表示...

    C++程序设计实践教程-马光志-习题答案

    此外,本章节还探讨了抽象程序设计的方法和程序结构与组织的基本原则。 1. **机器语言与汇编语言**:机器语言是最底层的计算机语言,由二进制指令组成;汇编语言则是机器语言的一种符号表示形式,它将二进制指令...

    YanceSpring-F-IM-master_java_impacket-master_

    1. **JAVA编程**: 包括面向对象的设计原则,异常处理,集合框架,多线程,网络编程,数据库连接(JDBC)等。 2. **即时通讯系统架构**: 如客户端-服务器模型,长连接,心跳机制,消息推送等。 3. **网络协议**: TCP/...

    2020-2021《C++语言程序设计》期末课程考试试卷B(含答案).docx

    - 学会识别何时使用何种设计模式,并能够灵活应用。 3. **软件工程原则**: - 软件生命周期各阶段的目标与任务,包括需求分析、设计、编码、测试和维护等。 - 代码质量的重要性,如何编写易于维护和扩展的高质量...

    基于WEB的水库水情自动测报系统的研究与设计(论文+源码)-kaic.docx

    - **系统设计原则**:遵循开放性、可扩展性、易用性等原则。 - **系统设计目标**:实现高效、稳定的数据处理能力。 - **主要开发工具**: - **WEB技术**:使用HTML、CSS、JavaScript等前端技术。 - **ASP.NET技术*...

    EN60204-1 安全标准

    通过以上内容的详细介绍,我们可以看出EN60204-1标准涵盖了电气设备从设计、制造到使用的各个方面,旨在确保电气设备的安全性和可靠性。这一标准对于电气设备的设计者、制造商以及使用者都具有重要的指导意义。

    Laravel开发-rest-handler

    合理设计异常处理逻辑,避免过度消耗系统资源,可以使用缓存技术、减少数据库查询等方式提高处理速度。 总之,`Laravel开发-rest-handler`是提升REST API质量的重要一环,它使得异常处理更加规范、人性化,增强了...

    阿里巴巴Java开发手册终极版v1.3.0.zip

    - **异常设计**:异常应当具有自解释性,避免过度封装系统异常。 - **并发设计**:推荐使用并发设计模式,如生产者消费者模型、读写锁策略等,以提高并发处理能力。 4. **框架规约** - **Spring**:合理使用AOP...

    MoreEffectiveC++

    - 使用异常规范可能会降低代码的灵活性; - C++11之后不再推荐使用异常规范。 #### Item15: 理解异常处理的成本 - **定义**:异常处理机制虽然强大,但也带来了一定的性能开销。 - **应用场景**: - 在性能敏感...

    黑马面试题总结

    以上总结了IT面试中常见的知识点,涵盖了进程与线程状态、输入输出流、集合框架、Java内存模型、多态、JDK 1.5新特性、设计模式、Java中的锁机制、JVM基础知识以及异常处理等多个方面。通过掌握这些核心概念和技术,...

    S2-2-MySchool数据库设计优化(PPT+源码)【第九章】

    1. 数据库设计基础:首先,了解数据库设计的基本原则,包括关系模型、范式理论(第一范式到第五范式)以及实体-关系(E-R)模型。理解这些基本概念有助于构建清晰、无冗余的数据结构。 2. 规范化理论:MySchool...

    c-c++常见面试题总结

    在C和C++的世界里,面试题通常涵盖了广泛的主题,从基本语法到高级设计原则,再到内存管理和模板元编程。以下是一些常见的C++面试题及其解析,它们可以帮助你为面试做好充分准备。 1. **指针与引用** - 指针是C++...

    开关电源设计(WORD版)

    - **选择原则**:根据设计目标和应用场景选择合适的电路拓扑结构。 - **常见拓扑**:包括Buck、Boost、Buck-Boost、Flyback等。 - **性能比较**:对比不同拓扑结构的优缺点,选择最适合的设计方案。 #### 三、元件...

    java面试葵花宝典

    #### 运行时异常与一般异常有何异同 - **运行时异常**: 通常是由编程错误引起的异常,例如`NullPointerException`,可以在运行时被抛出。 - **一般异常**: 通常是预期的异常情况,如`IOException`,开发者应该捕获并...

    阿里巴巴开发规范手册-泰山版

    手册中可能包含如何正确记录和分类异常,以及何时、何地抛出异常的指导原则。日志应当提供足够的信息以帮助开发者定位问题,同时避免过多的日志输出影响系统性能。 **单元测试**部分则强调了测试驱动开发(TDD)的...

Global site tag (gtag.js) - Google Analytics