异常使用指南

929 阅读10分钟

1. 合理使用异常

  • 问题

    先来看一个反例:

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

    }

    在上面这个企图利用数组越界的异常从而跳出死循环。如果针对的是数组遍历的场景,这么做的目的是企图利用Java异常机制,来跳过遍历中每次都需要检查是否越界来达到性能优化。但是,这种用法显然是不合理的,那么,对异常的使用应该注意哪些问题?

  • 答案

    1. 异常应该只用于异常处理的情况,永远不应该应用到控制流中, 例如,上面的反例;
    2. 异常机制的设计初衷是用于不正常的情形,所以JVM实现不会对其优化,所以企图用异常来达到性能优化,是不可行的;
    3. 把代码放在try-catch块中反而阻止了现代JVM可能会执行的一些优化操作;
    4. 针对上例,对数组标准的遍历模式并不会导致冗余的检查,JVM会对其进行优化;
  • 结论

    在使用异常的时候,应该坚持异常只应该被当做异常来处理,而不应该企图利用异常来达到性能优化的目的

2. 避免使用不必要的受检异常

  • 问题

    受检异常通过强制调用者通过catch处理异常情况,能够从一定程度上确保程序的可靠性。如果API设计时,抛出多个受检异常,那么调用方就必须使用多个catch进行相应的处理,或者将它们在throws出去,反而让调用方增加了负担。那么,在设计方法是否抛出异常的时候应该有哪些原则?

  • 问题

    1. 在设计方法抛出异常的时候,应该换位思考,如果自己是调用方使用你设计的API,处理你抛出的异常应该怎样处理?如果有明确的思路,就说明这个异常抛出来是合适的。也就是说,异常是API中不可避免的,并且一旦产生,调用方能够合理的处理,就说明抛出受检异常是合适的,一定要求这两个前提条件全部成立;

    2. “把调用具有受检异常的方法进行重构”的一种方法是,把这个抛出异常的方法分成两个方法,其中第一个方法返回一个boolean,表明状态测试,代表是否应该抛出异常,例如下面的示例代码:

      //Invocation with checked exception
      try {
           obj.action(args);
      } catch(TheCheckedException e) {
          // Handle exceptional condition
      }
      重构为:
      // Invocation with state-testing method and unchecked exception
      if (obj.actionPermitted(args))  { 
            obj.action(args);.
      } else {
             // Handle exceptional condition
      }
      

      上面的这种重构方式虽然代码调用更加复杂,但是让程序更有灵活性。但是,需要注意的时候,凡是涉及到状态更新和状态测试的时候,都需要考虑线程安全的问题

  • 结论

    由于受检异常,需要调用方必须做出处理,因此,在设计API的时候就需要着重去考虑所抛出的受检异常,是否真的是合理的,要站在调用方的角度去考虑。

3. 异常需要相应的文档

  • 问题

    在设计方法API时,异常是应该着重关注的,同样地,方法的文档注释上异常条件也需要显式的说明,因此,在针对异常编写文档时,有哪些需要注意的地方?

  • 答案

    1. **始终要单独地声明受检异常,**并且使用javadoc的@throws标记,准确地记录下抛出每个异常的条件。如果方法抛出多个异常类,不要使用它抛出异常类的父类,永远不要声明方法”throwsException”,或更糟糕的声明它”throw Throwable”。这样的声明没有给开发者关于”这个方法抛出哪些异常”的任何有用信息,实际上掩盖了该方法在同样的执行环境下可能抛出的任何其他异常, 因此会妨碍该方法的使用;
    2. 使用javacdoc的@throws标签可以显示方法可能抛出的运行时异常,但是不要使用throws关键字将运行时异常包含在方法的声明中。使用API的程序员必须知道哪些异常是需要受检的,哪些是不需要受检的。
  • 结论

    要为你编写的每个方法所抛出的每个异常建立文档,对受检的异常和未受检的异常,就像对于抽象和具体的方法都一样。要为每个受检的异常提供单独的throws字句,不要为未受检的异常提供throws语句。如果没有为可以抛出的异常建立文档,那么其他开发人员很难或者根本不可能有效使用你的类或接口。

4. 编写合适的异常信息

  • 问题

    当程序由于未被捕获的异常而失败的时候,系统会自动地打印出该异常的堆栈轨迹。在堆栈轨迹中包含该异常的字符串表示法(string representation),即它的toString方法的调用结果。它通常包含该异常的类名,紧随其后的是细节消息(detail message)。程序员会根据打印出来的异常信息进行排错,也就是说,如果异常信息不够充足的话,会减缓排错的速度,那么,应该如何编写合适的异常信息?

  • 答案

    1. 有一个很重要的原则,异常的细节消息应该清晰的描述出异常,便于以后分析。为了清晰的描述异常,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值。例如,IndexOutOfBoundsException异常的细节消息应该包含下界、上界以及没有落在界内的下标值。该细节消息提供了许多关于失败的信息。这三个值中任何一个或者全部都有可能是错的。实标的下标值可能小于下界或等于上界(“越界错误”),或者它可能是个无效值,太小或太大。下界也有可能大于上界(严重违反内部约束条件的一种情况)。每一种情形都代表了不同的问题,如果程序员知道应该去查找哪种错误,就可以极大地加速排错过程;为了确保在异常的细节消息中包含足够的能捕获失败的信息,一种办法是在异常的构造器中引入这些信息
    2. 堆栈轨迹的用途是与源文件结合起来进行分析,它通常包含抛出该异常的确切文件和行数,以及堆栈中所有其他方法调用所在的文件和行数。因此,如果描述异常信息量过多,实际上也是冗余的,有些信息可以通过阅读源代码而获得;
    3. 异常的细节消息不应该与“用户层次的错误消息”混为一谈,后者对于最终用户而言必须是可理解的。与用户层次的错误消息不同,异常的字符串表示法主要是让程序员或者域服务人员用来分析失败的原因。
  • 结论

    当异常被捕获的时候,如果有关键的异常信息的时候,可以很方便的进行排错,添加关键的异常信息是很有必要的。

5. 尽量异常封装

  • 问题

    如果方法抛出的异常与它所执行的任务没有明显的联系,**这种情形物会使人不知所措。**当方法将低层异常在高层继续抛出的时候,往往会发生这种情况。除了使人感到困惑之外,这也让实现细节污染了更高层的API。如果高层的实现在后续的发行版本中发生了变化,它所抛出的异常也可能会跟着发生变化,从而潜在地破坏现有的客户端程序。那么,在高低层API处理异常时应该怎么做呢?

  • 答案

    1. 更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法被称为异常转译(exceptiontranslation),如下所示:

      try{
          //Use lower-level abstraction to do our bidding
          ...
      }cache(LowerLevelException e){
          throw new HigherLevelException(...);
      }
      

      这种方式要求在高层API中,捕获到底层异常的时候抛出与高层业务意义吻合的高层异常HighLevelException,而不是直接将底层异常继续抛出去。

    2. 一种特殊的异常转译形式称为**异常链(exceptionchaining),**如果低层的异常对于调试导致髙层异常的问题非常有帮助,使用异常链就很合适。**低层的异常(原因)被传到髙层的异常,**髙层的异常提供访问方法(Thmwable.getCause)来获得低层的异常。例如:

      try{
          //Use lower-level abstraction to do our bidding
      }cache(LowerLevelException cause){
          throw new HigherLevelException(cause);
      }
      

      HighLevelException的构造器为:

      class HigherLevelException extends Exception {
          HigherLevelExceptionCThrowable cause) {
              super(cause);
          }
      }
      
    3. 面对异常到底应该怎样做?

      • **尽管异常转译与直接在高层将底层的异常继续抛出相比有所改进,但是它也不能被滥用。**如有可能,处理来自低层异常的最好做法是,应该极力避免底层异常的发生。有时候,可以在给低层传递参数之前,检査更高层方法的参数的有效性,从而避免低层方法抛出异常;
      • 如果无法避免低层异常,次选方案是,让高层API中通过判断绕开这些异常,从而将高层方法的调用者与低层的问题隔离开来。在这种情况下,可以使用日志工具进行记录。
  • 结论

    如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,除非低层方法碰巧可以保证它抛出的所有异常对髙层也合适才可以将异常从低层传播到高居。异常链对髙层和低层异常都提供了最佳的功能:它允许抛出适当的髙层异常,同时又能捕获底层的原因进行失败分析。

6. 不要忽略异常

  • 问题

    有这样的反例:

    try {
        ...
    }catch (SomeException e) {
    
    
    }
    

    }

    上面例子,虽然捕获了异常,但是是空的catch块,会不经意间忘记处理异常,这样会埋下隐患,面对异常应该有哪些基本的原则?

  • 答案

    1. 千万不要忽略异常:尽管这条原则是很显而易见的,但是它却常常被违反,当API的设计者声明一个方法将抛出某个异常的时候,他们等于正在试图说明一些危险情况,所以,千万不要忽略;
    2. 这条原则同样适用于受检异常和未受检的异常,不管异常代表了可预见的异常条件,还是编程错误,用空的catch块忽略它,将会导致程序在遇到错误的情况下悄然地执行下去。然后,有可能在将来的某个点上,当程序不能再容忍与错误源明显相关的问题时,它就会让系统失败。
  • 结论

    面对异常,最基本最重要的原则是,不要忽略异常,必须对异常进行处理。