《Effective Java》读书笔记

Chapter 9 Exceptions

Item 57: Use exceptions only for exceptional conditions

这条item的意思就是,千万不要用exception来控制control flow的终止,比如:

// Horrible abuse of exceptions. Don't ever do this!
try {
  int i = 0;
  while(true)
    range[i++].climb();
} catch(ArrayIndexOutOfBoundsException e) {
}

这段代码用异常来终止循环,这根本就不是异常的归宿。异常仅仅是为了exceptional conditions而存在的。此外,上面这种做法的坏处一大堆,根本列举不完。
一个好的API设计不会强制client用异常来完成控制流的终止,比如Iterator只有next()而没有hasNext()的话,那么由于next()可能会抛出异常所以client不得不catch住才知道没有元素可以拿了。
你也可以通过返回一个特殊的值(比如null)来告诉client:有问题啦!如果想让client在不用同步的情况下,并发地访问你的类,那么就可以用这种方法,因为如果用刚才那种“先检查,再调用”的方法的话,可能“先检查”完之后,状态被另一个线程改变了,那么“再调用”的时候还是可能抛出异常。除了这种情况外,大部分情况下都应该用“先检查,再调用”的方法(因为会让你的错误用法很容易被发现,比如你忘了先检查,那么就会抛出异常告诉你你代码有问题)。

Item 58: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors

有三种Throwable,分别是checked exceptions,runtime exceptions,和errors。checked exceptions一般用于可以恢复的情况。对于一个声明了throws的API方法,类库实现者强制client要处理这个异常。而另外两种Throwable一般不需要被catch,如果一个程序抛出了一个这样的非checked的异常,那么通常是不可恢复的,否则可能do more harm than good。runtime exceptions一般代表程序错误(代码写的有问题),比如ArrayIndexOutOfBoundsException。而errors,按照惯例,是被JVM保留用于表示资源不足等 使程序无法继续执行的异常。所以你不应该继承Error这个类,所以你自己实现的所有的unchecked异常都应该是继承自RuntimeException这个类。上面都用了“一般”这个词,因为情况并不是绝对的,比如有时候资源耗尽可能是由于你的程序错误导致的。在自定义异常中,你可以定义各种方法,从而给catch到这个异常的代码提供更多的信息。

Item 59: Avoid unnecessary use of checked exceptions

有时候定义一些不必要的checked exception会给client带来额外的负担,因为你强制他们要处理你的异常。如果说client遇到了这个unchecked exception最多只能作如下处理:

} catch(TheCheckedException e) {
    throw new AssertionError(); // 不可能发生!
}
或者:
} catch(TheCheckedException e) {
    e.printStackTrace(); // Oh well, we lose.
    System.exit(1);
}

那么你就可以考虑是不是可以把你的checked异常变成unchecked了。一种技巧就是把你的方法拆成两个方法,一个负责检查,另一个抛出一个unchecked异常。也就是item57里面说的“先检查,再调用”。

Item 60: Favor the use of standard exceptions

为了更好的代码复用和通用性,我们应该尽量使用Java platform libraries提供的异常。下面介绍一些最常用的Java平台类库提供的unchecked exception。IllegalArgumentException不用说了。IllegalStateException表示对象的状态有问题,比如还未完全初始化。ConcurrentModificationException和UnsupportedOperationException也都不用说了。另外,选择哪个异常并不需要那么的精确,比如在方法验证参数的时候也可以抛出NullPointerException,如果你要求参数不能为空的话。

Item 61: Throw exceptions appropriate to the abstraction

higher layers应该catch住lower-level exceptions,并抛出exceptions that can be explained in terms of the higher-level abstraction。否则的话,client可能会收到一条莫名的异常,而且这些lower-level exceptions属于你的内部实现,不应该propagate出去。比如:

/**
* Returns the element at the specified position in this list.
* @throws IndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= size()}).
*/
public E get(int index) {
    ListIterator<E> i = listIterator(index);
    try {
        return i.next();
    } catch(NoSuchElementException e) {
        throw new IndexOutOfBoundsException("Index: " + index);
    }
}

这里由于上面的specification的规定,所以把NoSuchElementException转换成了IndexOutOfBoundsException,我们把这种技巧叫做exception translation,如果底层异常对于client来说是make sense的,那么就可以不用translate。
有时候为了调试的目的,可以把lower-level exception(the cause)pass给higher-level exception:

// Exception Chaining
try {
    ... // Use lower-level abstraction
} catch (LowerLevelException cause) {
    throw new HigherLevelException(cause);
}

然后可以在HigherLevelException上用getCause()得到LowerLevelException。大多数exception都可以直接用这种constructor的形式传给他一个cause,对于有些没有这种constructor的异常,可以用Throwable.initCause方法。
但是exception translation不可滥用,最好还是能把client和这些底层调用分离,比如你可以在higher layer上默默地处理一下这些底层异常,或者在传给底层方法之前先验证一下参数。

Item 62: Document all exceptions thrown by each method

要在方法声明中throws具体的类,千万别为了省事儿,throws一个 所有可能抛出的异常的super class。所以千万不要throws Exception或者throws Throwable,这一点感觉和CLR via C#中的千万不要catch(Exception e)类似。正确做法是把每一个可能抛出的checked异常都用一个 @throws tag。
你应该尽量把可能抛出的unchecked异常也通过 @throws tag写在文档里,特别是在interface的文档里,应该说明可能抛出的unchecked异常,从而保证不同的实现的行为是一致的。

Item 63: Include failure-capture information in detail messages

这条item的意思就是:当一个程序fails due to an uncaught exception的时候,系统会自动print这个异常的stack trace,而这个stack trace包含这个异常的“toString”,通常来说这是唯一有用的信息,所以这个信息必须包括尽量多的有用的信息,以帮助查找问题根源。这个信息里面应该包括所有导致异常的参数的值,如IndexOutOfBoundsException就应该包括上界index、下界index和实际造成异常的index的值。但是并不是让你在这个信息中包含大量的文字,因为stack trace中会包括是在哪个源文件出错的,以及出错地方的行号,我们可以阅读这些源码来获取更多信息。另外,这个信息并不是给终端用户看的,所以可理解性并不是第一位的。综上,IndexOutOfBoundsException若是有这么一个constructor就好了:

/**
* Construct an IndexOutOfBoundsException.
*
* @param lowerBound the lowest legal index value.
* @param upperBound the highest legal index value plus one.
* @param index the actual index value.
*/
public IndexOutOfBoundsException(int lowerBound, int upperBound,int index) {
    // Generate a detail message that captures the failure
    super("Lower bound: " + lowerBound +
               ", Upper bound: " + upperBound +
               ", Index: " + index);
    // Save failure information for programmatic access
    this.lowerBound = lowerBound;
    this.upperBound = upperBound;
    this.index = index;
}

Item 64: Strive for failure atomicity

一个抛出异常的方法如果能让对象的状态还是在调用这个方法之前的状态(leave the object in the state that it was in prior to the invocation),那么这个方法就是failure atomic的,也就是可以恢复的,所以对checked exception来说应该是这样。
在immutable的对象上调用方法都是failure atomic的,就算失败,也只不过可能是阻止了一个新对象的创建,但不可能会破坏这个immutable对象的状态。
对于mutable的对象来说,最好的办法就是在执行真正的操作之前,先检查参数或者状态是否valid,比如Stack中的pop方法:

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

如果把上面代码中的“检查”去掉,那么如果是个empty stack的话,虽然也会抛出异常,但是size已经变成负数了,这时候状态就被污染了。
类似的,你也可以通过先做一些不会改变对象状态的操作(或者说计算),前提是“如果这时候没报错,那么后面的 改变对象状态的操作 就肯定不会报错”,然后再做一些改变对象状态的操作。比如向TreeSet中插入一个元素的方法,会先在这个TreeSet中搜索这个元素,如果搜索没报错,再插入。
还有一种比较少用的方法在捕获到异常后,写一些“recovery code”,从而roll back到之前的状态。
还有一种方法是,先拷贝一份这个对象,然后在这份临时的拷贝上进行修改操作,如果成功了,再copy回去。
但是当多个线程对同一个对象concurrently地进行修改操作的话,这个对象很可能会被搞成inconsistent state,比如在出现ConcurrentModificationException之后,就不应该认为这个对象还能被用了(unrecoverable)。

Item 65: Don’t ignore exceptions

这条item就是告诉我们,别做这种事儿:

// Empty catch block ignores exception - Highly suspect!
try {
    ...
} catch (SomeException e) {
}

你至少也应该加一条注释,说明为什么可以忽略这个异常。
我记得在CLR via C#中这种做法被叫做silence或者swallow一个异常。

原文地址:https://www.cnblogs.com/raytheweak/p/7224688.html