Effective Java (异常处理)

五十七、只针对异常情况才使用异常:

不知道你否则遇见过下面的代码:

代码如下:

try {
              int i = 0;3
       while (true)   
       range[i++].climb();
       }
       catch (ArrayIndexOutOfBoundsException e) {
       }

这段代码的意图不是很明显,其本意就是遍历变量数组range中的每一个元素,并执行元素的climb方法,当下标超出range的数组长度时,将会直接抛出ArrayIndexOutOfBoundsException异常,catch代码块将会捕获到该异常,但是未作任何处理,只是将该错误视为正常工作流程的一部分来看待。这样的写法确实给人一种匪夷所思的感觉,让我们再来看一下修改后的写法:


代码如下:

for (Mountain m : range) {
        m.climb();
 }

和之前的写法相比其可读性不言而喻。那么为什么又有人会用第一种写法呢?显然他们是被误导了,他们企图避免for-each循环中JVM对每次数组访问都要进行的越界检查。这无疑是多余的,甚至适得其反,因为将代码放在try-catch块中反而阻止了JVM的某些特定优化,至于数组的边界检查,现在很多JVM实现都会将他们优化掉了。在实际的测试中,我们会发现采用异常的方式其运行效率要比正常的方式慢很多。
      除了刚刚提到的效率和代码可读性问题,第一种写法还会掩盖一些潜在的Bug,假设数组元素的climb方法中也会访问某一数组,并且在访问的过程中出现了数组越界的问题,基于该错误,JVM将会抛出ArrayIndexOutOfBoundsException异常,不幸的是,该异常将会被climb函数之外catch语句捕获,在未做任何处理之后,就按照正常流程继续执行了,这样Bug也就此被隐藏起来。
      这个例子的教训很简单:"异常应该只用于异常的情况下,它们永远不应该用于正常的控制流"。虽然有的时候有人会说这种怪异的写法可以带来性能上的提升,即便如此,随着平台实现的不断改进,这种异常模式的性能优势也不可能一直保持。然而,这种过度聪明的模式带来的微妙的Bug,以及维护的痛苦却依然存在。
      根据这条原则,我们在设计API的时候也是会有所启发的。设计良好的API不应该**它的客户端为了正常的控制流而使用异常。如Iterator,JDK在设计时充分考虑到这一点,客户端在执行next方法之前,需要先调用hasNext方法已确认是否还有可读的集合元素,见如下代码:

代码如下:

for (Iterator i = collection.iterator(); i.hasNext(); ) {
     Foo f = i.next();
     }

如果Iterator缺少hasNext方法,客户端则将**改为下面的写法:


代码如下:

try {
 Iterator i = collection.iterator();
 while (true)
 Foo f = i.next();
 }
 catch (NoSuchElementException e) {
 }

这应该非常类似于本条目开始时给出的遍历数组的例子。在实际的设计中,还有另外一种方式,即验证可识别的错误返回值,然而该方式并不适合于此例,因为对于next,返回null可能是合法的。那么这两种设计方式在实际应用中有哪些区别呢?
      1. 如果是缺少同步的并发访问,或者可被外界改变状态,使用可识别返回值的方法是非常必要的,因为在测试状态(hasNext)和对应的调用(next)之间存在一个时间窗口,在该窗口中,对象可能会发生状态的变化。因此,在该种情况下应选择返回可识别的错误返回值的方式。
      2. 如果状态测试方法(hasNext)和相应的调用方法(next)使用的是相同的代码,出于性能上的考虑,没有必要重复两次相同的工作,此时应该选择返回可识别的错误返回值的方式。
      3. 对于其他情形则应该尽可能考虑"状态测试"的设计方式,因为它可以带来更好的可读性。

五十八、对可恢复的情况使用受检异常,对编程错误使用运行时异常:

Java中提供了三种可抛出结构:受检异常、运行时异常和错误。该条目针对这三种类型适用的场景给出了一般性原则。
      1. 如果期望调用者能够适当地恢复,对于这种情况就应该使用受检异常,如某人打算网上购物,结果余额不足,此时可以抛出自定义的受检异常。通过抛出受检异常,将**调用者在catch子句中处理该异常,或继续向上传播。因此,在方法中声明受检异常,是对API用户的一种潜在提示。
      2. 用运行时异常来表明编程错误。大多数的运行时异常都表示"前提违例",即API的使用者没有遵守API设计者建立的使用约定。如数组访问越界等问题。
      3. 对于错误而言,通常是被JVM保留用于表示资源不足、约束失败,或者其他使程序无法继续执行的条件。
      针对自定义的受检异常,该条目还给出一个非常实用的技巧,当调用者捕获到该异常时,可以通过调用该自定义异常提供的接口方法,获取更为具体的错误信息,如当前余额等信息。

五十九、避免不必要的使用受检异常:

受检异常是Java提供的一个很好的特征。与返回值不同,它们**程序员必须处理异常的条件,从而大大增强了程序的可靠性。然而,如果过分使用受检异常则会使API在使用时非常不方便,毕竟我们还是需要用一些额外的代码来处理这些抛出的异常,倘若在一个函数中,它所调用的五个API都会抛出异常,那么编写这样的函数代码将会是一项令人沮丧的工作。
      如果正确的使用API不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员可以立即采用有用的动作,这种负担就被认为是正当的。除非这两个条件都成立,否则更适合使用未受检异常,见如下测试:


代码如下:

try {
      dosomething();
      } catch (TheCheckedException e) {
      throw new AssertionError();
      }

try {
    donsomething();
    } catch (TheCheckedException e) {
    e.printStackTrace();
    System.exit(1);
    }

当我们使用受检异常时,如果在catch子句中对异常的处理方式仅仅如以上两个示例,或者还不如它们的话,那么建议你考虑使用未受检异常。原因很简单,它们在catch子句中,没有做出任何用于恢复异常的动作。

六十、优先使用标准异常:

使用标准异常,不仅可以更好的复用已有的代码,同时也使你设计的API更加容易学习和使用,因为它和程序员已经熟悉的习惯用法更为一致。另外一个优势是,代码的可读性更好,程序员在阅读时不会出现更多的不熟悉的代码。该条目给出了一些非常常用且容易被复用的异常,见下表:
      异常                                               应用场合
      IllegalArgumentException              非null的参数值不正确。
      IllegalStateException                     对于方法调用而言,对象状态不合适。
      NullPointerException                     在禁止使用null的情况下参数值为null。
      IndexOutOfBoundsException         下标参数值越界
      ConcurrentModificationException   在禁止并发修改的情况下,检测到对象的并发修改。
      UnsupportedOperationException    对象不支持用户请求的方法。
      当然在Java中还存在很多其他的异常,如ArithmeticException、NumberFormatException等,这些异常均有各自的应用场合,然而需要说明的是,这些异常的应用场合在有的时候界限不是非常分明,至于该选择哪个比较合适,则更多的需要依赖上下文环境去判断。
      最后需要强调的是,一定要确保抛出异常的条件和该异常文档中描述的条件保持一致。

六十一、抛出与抽象相对应的异常:

如果方法抛出的异常与它所执行的任务没有明显的关系,这种情形将会使人不知所措。特别是当异常从底层开始抛出时,如果在中间层没有做任何处理,这样底层的实现细节将会直接污染高层的API接口。为了解决这样的问题,我们通常会做出如下处理:

代码如下:

try {
  doLowerLeverThings();
  } catch (LowerLevelException e) {
  throw new HigherLevelException(...);
  }

这种处理方式被称为异常转译。事实上,在Java中还提供了一种更为方便的转译形式--异常链。试想一下上面的示例代码,在调试阶段,如果高层应用逻辑可以获悉到底层实际产生异常的原因,那么对找到问题的根源将会是非常有帮助的,见如下代码:


代码如下:

try {         doLowerLevelThings();
          } catch (LowerLevelException cause) {
   throw new HigherLevelException(cause);
   }

底层异常作为参数传递给了高层异常,对于大多数标准异常都支持异常链的构造器,如果没有,可以利用Throwable的initCause方法设置原因。异常链不仅让你可以通过接口函数getCause访问原因,它还可以将原因的堆栈轨迹集成到更高层的异常中。
      通过这种异常链的方式,可以非常有效的将底层实现细节与高层应用逻辑彻底分离出来。

六十三、在细节中包含能捕获失败的信息:

当程序由于未被捕获的异常而失败的时候,系统会自动地打印出该异常的堆栈轨迹。在堆栈轨迹中包含该异常的字符串表示法,即toString方法的返回结果。如果我们在此时为该异常提供了详细的出错信息,那么对于错误定位和追根溯源都是极其有意义的。比如,我们将抛出异常的函数的输入参数和函数所在类的域字段值等信息格式化后,再打包传递给待抛出的异常对象。假设我们的高层应用捕捉到IndexOutOfBoundsException异常,如果此时该异常对象能够携带数组的下界和上界,以及当前越界的下标值等信息,在看到这些信息后,我们就能很快做出正确的判断并修订该Bug。
    特别是对于受检异常,如果抛出的异常类型还能提供一些额外的接口方法用于获取导致错误的数据或信息,这对于捕获异常的调用函数进行错误恢复是非常重要的。

六十四、努力使失败保持原子性:

这是一个非常重要的建议,因为在实际开发中当你是接口的开发者时,经常会忽视他,认为不保证的话估计也没有问题。相反,如果你是接口的使用者,也同样会忽略他,会认为这个是接口实现者理所应当完成的事情。
      当对象抛出异常之后,通常我们期望这个对象仍然保持在一种定义良好的可用状态之中,即使失败是发生在执行某个操作的过程中间。对于受检异常而言,这尤为重要,因为调用者希望能从这种异常中进行恢复。一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有"失败原子性"。
      有以下几种途径可以保持这种原子性。
      1. 最简单的方法是设计不可变对象。因为失败的操作只会导致新对象的创建失败,而不会影响已有的对象。
      2. 对于可变对象,一般方法是在操作该对象之前先进行参数的有效性验证,这可以使对象在被修改之前,抛出更为有意义的异常,如:


代码如下:

public Object pop() {
  if (size == 0)
  throw new EmptyStackException();
  Object result = elements[--size];
  elements[size] = null;
  return result;
  }

如果没有在操作之前验证size,elements的数组也会抛出异常,但是由于size的值已经发生了变化,之后再继续使用该对象时将永远无法恢复到正常状态了。
      3. 预先写好恢复性代码,在出现错误时执行带段代码,由于此方法在代码编写和代码维护的过程中,均会带来很大的维护开销,再加之效率相对较低,因此很少会使用该方法。
      4. 为该对象创建一个临时的copy,一旦操作过程中出现异常,就用该复制对象重新初始化当前的对象的状态。
      虽然在一般情况下都希望实现失败原子性,然而在有些情况下却是难以做到的,如两个线程同时修改一个可变对象,在没有很好同步的情况下,一旦抛出ConcurrentModificationException异常之后,就很难在恢复到原有状态了。

六十五、不要忽略异常:

这是一个显而易见的常识,但是经常会被违反,因此该条目重新提出了它,如:


代码如下:

try {
dosomething();
} catch (SomeException e) {
}

可预见的、可以使用忽略异常的情形是在关闭FileInputStream的时候,因为此时数据已经读取完毕。即便如此,如果在捕获到该异常时输出一条提示信息,这对于挖出一些潜在的问题也是非常有帮助的。否则一些潜在的问题将会一直隐藏下去,直到某一时刻突然爆发,以致造成难以弥补的后果。
      该条目中的建议同样适用于受检异常和未受检的异常。

(0)

相关推荐

  • java异常处理详细介绍及实例

    Java异常层次结构 Exception异常 RuntimeException与非RuntimeException异常的区别: 非RuntimeException(检查异常):在程序中必须使用try-catch进行处理,否则程序无法编译. RuntimeException:可以不使用try-catch进行处理,但是如果有异常产生,则异常将由JVM进行处理. 比如:我们从来没有人去处理过NullPointerException异常,它就是运行时异常,并且这种异常还是最常见的异常之一. 出现运行时异

  • Java异常处理实例分析

    本文实例讲述了Java异常处理的用法.分享给大家供大家参考.具体分析如下: Java的异常处理机制可以帮助我们避开或者处理程序可能发生的错误,从而使得程序在遇到一些可恢复的错误的时候不会意外终止,而是去处理这些错误,也使得我们在写程序的时候不必写大量的代码来检查错误情况,增强了代码的可读性和逻辑性.在Java中,异常代表一个错误的实体对象. 异常可分为两类:一类是严重错误,如硬件错误.内存不足等,它们对应着java.lang包下的Error类及其子类.通常这类错误程序自身是无法恢复的,需要中断程

  • 详解Java异常处理中finally子句的运用

    当异常被抛出,通常方法的执行将作一个陡峭的非线性的转向.依赖于方法是怎样编码的,异常甚至可以导致方法过早返回.这在一些方法中是一个问题.例如,如果一个方法打开一个文件项并关闭,然后退出,你不希望关闭文件的代码被异常处理机制旁路.finally关键字为处理这种意外而设计. finally创建一个代码块.该代码块在一个try/catch 块完成之后另一个try/catch出现之前执行.finally块无论有没有异常抛出都会执行.如果异常被抛出,finally甚至是在没有与该异常相匹配的catch子句

  • Java中的异常处理用法及其架构和使用建议

    Java异常是Java提供的一种识别及响应错误的一致性机制. Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性.在有效使用异常的情况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了"什么"被抛出,异常堆栈跟踪回答了"在哪"抛出,异常信息回答了"为什么"会抛出. Java异常机制用到的几个关键字:try.catch.finally.throw.throws. 关键字 说

  • java多线程中的异常处理机制简析

    在java多线程程序中,所有线程都不允许抛出未捕获的checked exception,也就是说各个线程需要自己把自己的checked exception处理掉.这一点是通过java.lang.Runnable.run()方法声明(因为此方法声明上没有throw exception部分)进行了约束.但是线程依然有可能抛出unchecked exception,当此类异常跑抛出时,线程就会终结,而对于主线程和其他线程完全不受影响,且完全感知不到某个线程抛出的异常(也是说完全无法catch到这个异常

  • Java EE项目中的异常处理总结(一篇不得不看的文章)

    为什么要在J2EE项目中谈异常处理呢?可能许多java初学者都想说:"异常处理不就是try-.catch-finally吗?这谁都会啊!".笔者在初学java时也是这样认为的.如何在一个多层的j2ee项目中定义相应的异常类?在项目中的每一层如何进行异常处理?异常何时被抛出?异常何时被记录?异常该怎么记录?何时需要把checked Exception转化成unchecked Exception ,何时需要把unChecked Exception转化成checked Exception?异

  • 剖析Java中的事件处理与异常处理机制

    一.事件处理 其实,由事件处理这个名字自然就想到MFC中的消息响应机制,就我的体会,它们应该算是南桔北枳的情形吧,我怀疑Java中的事件处理这个"新瓶"应是装的MFC中的消息响应这个"旧酒".     所谓的"事件"即如键盘按键.鼠标点击等这类由动作或什么导致某个状态改变并需要对这个改变作相应响应的这类改变.我们可以将Java中的事件分为按钮.鼠标.键盘.窗口.其它事件这几大类.     事件处理模型  1.   基于继承的事件处理模型(JDK1

  • Java异常处理之try...catch...语句的使用进阶

    try就像一个网,把try{}里面的代码所抛出的异常都网住,然后把异常交给catch{}里面的代码去处理.最后执行finally之中的代码.无论try中代码有没有异常,也无论catch是否将异常捕获到,finally中的代码都一定会被执行. 虽然 Java 执行时期系统所提供的预设处理器对除错很有用,你通常想要自己处理例外.这样做有两个优点:第一,它让你修正错误.第二,它可以避免程式自动终止.每当错误发生时,如果你的程式就停止而且列印出堆叠追踪,大多数的使用者都会感到很困惑.很幸运,你很容易就能

  • Effective Java (异常处理)

    五十七.只针对异常情况才使用异常: 不知道你否则遇见过下面的代码: 复制代码 代码如下: try {              int i = 0;3       while (true)           range[i++].climb();       }        catch (ArrayIndexOutOfBoundsException e) {       } 这段代码的意图不是很明显,其本意就是遍历变量数组range中的每一个元素,并执行元素的climb方法,当下标超出ran

  • Effective Java 在工作中的应用总结

    目录 一  创建和销毁对象篇 1  若有多个构造器参数时,优先考虑构造器 2  通过私有构造器强化不可实例化的能力 二  类和接口篇 1  最小化类和成员的可访问性 2  使可变形最小化 三  泛型篇 1  列表优先于数组 四  方法篇 1  校验参数的有效性 2  谨慎设计方法签名 3  返回零长度的数组或者集合,而不是null 五  通用程序设计篇 1  如果需要精确的答案,请避免使用float和double 2  基本类型优先于装箱基本类型 六  异常 1  每个方法抛出的异常都要有文档

  • 老生常谈Java异常处理和设计(推荐)

    在程序设计中,进行异常处理是非常关键和重要的一部分.一个程序的异常处理框架的好坏直接影响到整个项目的代码质量以及后期维护成本和难度.试想一下,如果一个项目从头到尾没有考虑过异常处理,当程序出错从哪里寻找出错的根源?但是如果一个项目异常处理设计地过多,又会严重影响到代码质量以及程序的性能.因此,如何高效简洁地设计异常处理是一门艺术,本文下面先讲述Java异常机制最基础的知识,然后给出在进行Java异常处理设计时的几个建议. 若有不正之处,请多多谅解和指正,不胜感激. 以下是本文的目录大纲: 一.什

  • 深入理解java异常处理机制及应用

    1. 引子 try-catch-finally恐怕是大家再熟悉不过的语句了,而且感觉用起来也是很简单,逻辑上似乎也是很容易理解.不过,我亲自体验的"教训"告诉我,这个东西可不是想象中的那么简单.听话.不信?那你看看下面的代码,"猜猜"它执行后的结果会是什么?不要往后看答案.也不许执行代码看真正答案哦.如果你的答案是正确,那么这篇文章你就不用浪费时间看啦. package Test; public class TestException { public TestEx

  • Java异常处理运行时异常(RuntimeException)详解及实例

      Java异常处理运行时异常(RuntimeException)详解及实例 RuntimeException RunntimeException的子类: ClassCastException 多态中,可以使用Instanceof 判断,进行规避 ArithmeticException 进行if判断,如果除数为0,进行return NullPointerException 进行if判断,是否为null ArrayIndexOutOfBoundsException 使用数组length属性,避免越

  • 深入理解java异常处理机制的原理和开发应用

    Java异常处理机制其最主要的几个关键字:try.catch.finally.throw.throws,以及各种各样的Exception.本篇文章主要在基础的使用方法上,介绍了如何更加合理的使用异常机制. try-catch-finally try-catch-finally块的用法比较简单,使用频次也最高.try块中包含可能出现异常的语句(当然这是人为决定的,try理论上可以包含任何代码),catch块负责捕获可能出现的异常,finally负责执行必须执行的语句,这里的代码不论是否发生了异常,

  • 浅谈java异常处理之空指针异常

    听老师说,在以后的学习中大部分的异常都是空指针异常.所以抽点打游戏的时间来查询一下什么是空指针异常 一:空指针异常产生的主要原因如下: (1)当一个对象不存在时又调用其方法会产生异常obj.method() // obj对象不存在 (2)当访问或修改一个对象不存在的字段时会产生异常obj.method() // method方法不存在 (3)字符串变量未初始化: (4)接口类型的对象没有用具体的类初始化,比如: List lt:会报错 List lt = new ArrayList():则不会报

  • java异常处理的简单练习

    异常的练习: 老师用电脑上课. 开始思考上课中出现的问题. 比如问题是 电脑蓝屏. 电脑冒烟. 要对问题进行描述,封装成对象. 可是当冒烟发生后,出现讲课进度无法继续. 出现了讲师的问题:课时计划无法完成. class Teacher { private Computer cmp; public void shangKe()throws NoPlanException /*声明异常*/ { cmp=new Computer(); try { cmp.run(); } catch(LanPingE

  • 浅谈java异常处理(父子异常的处理)

    我当初学java异常处理的时候,对于父子异常的处理,我记得几句话"子类方法只能抛出父类方法所抛出的异常或者是其子异常,子类构造器必须要抛出父类构造器的异常或者其父异常".那个时候还不知道子类方法为什么要这样子抛出异常,后来通过学习<Thinking in Java>,我才明白其中的道理,现在我再来温习一下. 一.子类方法只能抛出父类方法的异常或者是其子异常 对于这种限制,主要是因为子类在做向上转型的时候,不能正确地捕获异常 package thinkinginjava; p

随机推荐