Effective Java 第二版 第9章 异常

9 异常

java的throwable结构分为三类:
(1)
运行时异常: 都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。

(2)
非运行时异常 (编译异常,受查异常、受检异常): 是RuntimeException以外的异常,类型上都 属于Exception类及其子类(不是RunTimeException的子类)。 从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
(3)Error

第57条 只针对异常的情况才使用异常

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

这是一种错误的写法,企图利用java的错误判断机制来提高性能,因为VM对每一次的数组访问都要检查越界情况,误认为正常的循环终止检查被编译器隐藏了。这种想法有三个错误:

  • 异常设计的初衷是用于不正常的情形,所以很少有JVM会对异常进行优化。
  • 把代码放在try-catch块中反而阻止了现代JVM本来要执行的某些特定的优化。
  • 使用标准的数组遍历方式并不会导致冗余的检查。现在JVM会对此做优化。

顾名思义,异常只应该用于异常的情况,异常永远不应该用于正常的控制流。

设计良好的api不应该强迫它的客户端为了正常的控制流程而使用异常。 如果类有“状态相关(state-dependent)”的方法(即只有在特定的不可预测的条件下才可以被调用的方法),那么这个类也应该有状态测试(state-testing)的方法(即指示是否可以调用这个状态相关的方法)。例如,Iterator有状态相关的next方法,和相应的状态测试方法hasNext。

“状态测试方法”和“可识别的返回值”这两种做法的指导原则:

  • 如果对象在缺少外部同步的情况下被并发访问,或者可被外界改变状态,使用“可识别的返回值”。因为在调用状态测试方法和状态相关方法之间,对象状态可能已经发生变化了。
  • 如果状态测试方法必须重复状态相关方法的工作,从性能角度考虑,使用“可识别的返回值”。
  • 如果所有其他方面都是等同的,优先使用“状态测试方法”。

第58条 对可恢复的情况使用受检异常,对编程错误使用运行时异常

Java语言提供了三种可抛出结构(throwable) : 受检异常(checked exception)、运行时异常(runtime exception)和错误(error)。

如果期望调用者能够适当地恢复,对于这种情况,就应该使用受检异常。 方法中声明要抛出的每一个受检异常,都是对api用户的一种潜在指示:与异常相关联的条件是调用这个方法的一种可能的结果。

运行时异常和错误都是不需要也不应该被捕获的可抛出结构。如果程序抛出未受检的异常或者错误,往往就属于不可恢复的情形,继续执行下去有害无益。

用运行时异常来表明编程错误。 大多数的运行时异常都表示提前违例(precondition violation)。前提违例是指api客户没有遵守api规范建立的约定。例如,数组的访问约定指明了数组的下标指的范围。ArrayIndexOutOfBoundsException表明这个前提被违反了。

按照惯例,错误被JVM保留用于表示资源不足、约束失败或者其他使程序无法继续执行的条件。因此最好不要再实现任何新的Error子类。 你所实现的所有的未受检的抛出结构都应该是RunTimeException的子类。

第59条 避免不必要的使用受检异常

如果正确地使用api并不能阻止这种异常条件的产生,并且一旦产生异常,使用api的程序员可以立即采取有用的动作。这种对程序员的负担认为是正当的。除非上述两个条件都成立,否则更适合使用非受检的异常。

第60条 优先使用标准的异常

IllegalArgumentException: 参数非法
IllegalStateException: 对象状态不对。例如,如果在某个对象被正确的初始化之前,调用者就企图使用这个对象,就会抛出这个异常。
NullPointerException
ArrayIndexOutOfBoundsException
ConcurrentModificationException: 如果一个对象被设计用于单线程或者与外部同步机制配合使用,一旦发现它正在(或者已经)被并发修改,就可以抛出这个异常。
UnsupportedOperationException:
ArithmeticException:
NumberFormatException:

第61条 抛出与抽象对应的异常

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

	此代码片段取自AbstractSequentialList/**
     * Returns the element at the specified position in this list.
     *
     * <p>This implementation first gets a list iterator pointing to the
     * indexed element (with <tt>listIterator(index)</tt>).  Then, it gets
     * the element using <tt>ListIterator.next</tt> and returns it.
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
    
    
        try {
    
    
            return listIterator(index).next();
        } catch (NoSuchElementException exc) {
    
    
            throw new IndexOutOfBoundsException("Index: "+index);
        }
    }

异常链(exception chaining)和支持链(chaining-aware):

    public static void main(String[] args) throws HigherLevelException {
    
    
        try {
    
    
            throw new LowerLevelException();
        } catch (LowerLevelException cause) {
    
    
            throw new HigherLevelException(cause);
        }
    }

public class HigherLevelException extends Exception {
    
    
    public HigherLevelException(Throwable cause) {
    
    
        super(cause);
    }
}

异常链中,低层的异常被传递到高层的异常,高层的异常提供访问方法来获得低层的异常。对于没有支持链的异常,可以利用Throwable的能力。initCause设置异常原因,getCause可以访问到原因。

    public synchronized Throwable initCause(Throwable cause) {
    
    
        if (this.cause != this)
            throw new IllegalStateException("Can't overwrite cause with " +
                                            Objects.toString(cause, "a null"), this);
        if (cause == this)
            throw new IllegalArgumentException("Self-causation not permitted", this);
        this.cause = cause;
        return this;
    }

    public synchronized Throwable getCause() {
    
    
        return (cause==this ? null : cause);
    }

异常转译不能滥用。调用低层方法之前确保它们会成功执行,从而避免它们抛出异常。检查参数的有效性是很有效果的。

第62条 每个方法抛出的异常都要有文档

始终要单独地声明受检的异常,并且利用javadoc的@throws标记,准确地记录下抛出每个异常的条件。

对于接口中的方法,在文档中记录下它可能抛出的未受检异常尤为重要。这份文档构成了该接口的**通用约定(general contract)**的一部分。指定了接口实现必须遵循的公共行为。

使用javadoc的@throws标记记录方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中。

如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档是可以接受的。

第63条 在细节消息中包含能捕获失败的信息

为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值。 例如,IndexOutOfBoundsException的细节信息应该包括:下界、上界和没有落在界内的下标值。

异常消息中的“硬数据”是非常重要的,但是包含大量的描述信息是没有必要的。在异常的构造器中传入这些“硬数据”是一种很好的方法。例如:

    以前IndexOutOfBoundsException有这种构造函数,现在没有了。
    public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {
    
    
        super(String.format("Lower bound: %d, Upper bound: %d, Index = %d"));
        this.lowerBound = lowerBound;
        this.upperBound = upperBound;
        this.index = index;
    }

第64条 努力使失败保持原子性

一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法具有失败原子性(failure atomic)

失败原子性的几种实现途径:

  • 不可变的对象
    如果操作失败了,会阻止创建新的对象。原来的对象处于原来的状态。
  • 对于可变对象,在执行操作之前检查参数的有效性。
  • 调整计算处理过程的顺序,任何可能会失败的计算部分都在对象状态被修改之前发生。
  • 编写恢复代码(recovery code)
    拦截操作过程中发生的失败,使对象回滚到操作开始之前的状态上。这种方法主要用于持久化的(基于硬盘的,disk-based)数据结构。
  • 在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。
    例如,Collections.sort在执行排序之前,首先把输入列表转到一个数组中,以便降低在排序的内循环中访问元素所需要的开销,在考虑了性能的同时增加了一项优势:即使排序失败,也能保证输入列表保持原样。
          public static <T extends Comparable<? super T>> void sort(List<T> list) {
          
          
          list.sort(null);
      }  
    
          default void sort(Comparator<? super E> c) {
          
          
          Object[] a = this.toArray();
          Arrays.sort(a, (Comparator) c);
          ListIterator<E> i = this.listIterator();
          for (Object e : a) {
          
          
              i.next();
              i.set((E) e);
          }
      } 
    

一般而言,作为方法规范的一部分,产生任何异常都应该让对象保持在被调用之前的状态。如果违反这条规则,API文档就应该指明对象会处于什么状态。

第65条 不要忽略异常

空的catch块会使异常达不到应有的目的,即强迫程序员去处理异常的情况。至少,catch块应该包含一条说明,解释为什么可以忽略这条异常。

正确地处理异常能够彻底挽回失败。只要将异常传播给外界,至少会导致程序迅速地失败,从而保留了有助于调试该失败的条件信息。

猜你喜欢

转载自blog.csdn.net/kaikai_sk/article/details/126728536