目录
- 避免不必要的使用受检异常
- 优先使用标准的异常
- 抛出与抽象对应的异常
- 每个方法抛出的异常都需要创建文档
- 在细节消息中包含失败一捕获信息
- 保持失败原子性
- 不要忽略异常
- 同步访问共享的可变数据
- 避免过度同步
- executor 、task 和 stream 优先于线程
71. 避免不必要的使用受检异常
方法中使用受检异常,会强迫调用方必须处理异常,增加了可靠性,但是像stream则无法处理受检异常(详见第 45 条至第 48 条)。
如果正确地使用 API 并不能阻止这种异常条件的产生,并且一旦产生异常,使用 API 的程序员可以立即采取有用的动作,这种负担就被认为是正当的。
除了用受检异常,还可以返回Optional方法,相比受检异常,缺点是无法描述无法调用的原因。
“把受检异常变成未受检异常”的一种方法是,把这个抛出异常的方法分成两个方法,其中第一个方法返回一个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 用起来更加舒服。虽然后者的调用序列没有前者漂亮,但是这样得到的 API 更加灵活。
72. 优先使用标准的异常
重用标准异常的好处:
1、更易于学习和使用
2、可读性会更好
3、异常类越少,jvm装载类越少,装载的类越少,占用内存就少
最常见的可重用异常类
异常 | 使用场合 |
---|---|
IllegalArgumentException | 非 null 的参数值不正确 |
IllegalStateException | 不适合方法调用的对象状态 |
NullPointerException | 在禁止使用 null 的情况下参数值为 null |
IndexOutOfBoundsExecption | 下标参数值越界 |
ConcurrentModificationException | 在禁止并发修改的情况下,检测到对象的并发修改 |
UnsupportedOperationException | 对象不支持用户请求的方法 |
73. 抛出与抽象对应的异常
如果方法抛出的异常与它所执行的任务没有明显的联系,这种情形将会使人不知所措。为了避免这个问题, 更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。
/* Exception Translation */
try {
... /* Use lower-level abstraction to do our bidding */
} catch ( LowerLevelException e ) {
throw new HigherLevelException(...);
}
例如这个异常就是RuntimeException的异常
/
**
* 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 );
}
}
一种特殊的异常转译形式称为异常链(exception chaining),如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适。低层的异常(原因)被传到高层的异常,高层的异常提供访问方法(Throwable 的getCause 方法)来获得低层的异常:
// Exception Chaining
try {
... // Use lower-level abstraction to do our bidding
} catch (LowerLevelException cause) {
throw new HigherLevelException(cause);
}
/* Exception with chaining-aware constructor */
class HigherLevelException extends Exception {
HigherLevelException( Throwable cause ) {
super(cause);
}
}
大多数标准的异常都有支持链的构造器,如果没有,可以调用Throwable的initCaluse(Throwable cause)
74. 每个方法抛出的异常都需要创建文档
始终要单独地声明受检异常, 并且利用 Javadoc 的 @throws 标签, 准确地记录下抛出每个异常的条件。
永远不要声明一个公有方法直接“ throws Exception”,因为它实际上掩盖了该方法在同样的执行环境下可能抛出的任何其他异常。这条建议有一个例外,就是 main 方法它可以被安全地声明抛出 Exception ,因为它只通过虚拟机调用
75. 在细节消息中包含失败一捕获信息
**为了捕获失败,异常的细节信息应该包含“对该异常有贡献”的所有参数和字段的值。**有助于开发人员定位问题。
对安全敏感的信息有一条忠告。由于在诊断和修正软件问题的过程中,许多人都可以看见堆栈轨迹, 因此千万不要在细节消息中包含密码、密钥以及类似的信息!
76. 保持失败原子性
**失败的方法调用应该使对象保持在被调用之前的状态。**实现的方法:
1、设计一个不可变的对象(即使失败,对象不可变,不会改变状态)
2、在执行操作之前检查参数的有效性。
例如这段代码,如果没有size校验,那么会改变size的状态,并且会抛出ArrayIndexOutOfBoundsException,报错信息与出错原因不一致。
public Object pop() {
if ( size == 0 )
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; /* Eliminate obsolete reference */
return(result);
}
3、拷贝对象,当操作完成之后再用临时拷贝中的结果代替对象的内容
4、写一段恢复代码恢复异常之前的状态
77. 不要忽略异常
空的 catch 块会使异常达不到应有的目的,假如真的发生异常,将无从查起。
如果选择忽略异常, catch 块中应该包含一条注释,说明为什么可以这么做,并且变量应该命名为 ignored:
Future<Integer> f = exec.submit(planarMap::chromaticNumber);
int numColors = 4; // Default: guaranteed sufficient for any map
try {
numColors = f.get( 1L, TimeUnit.SECONDS );
} catch ( TimeoutException | ExecutionException ignored ) {
// Use default: minimal coloring is desirable, not required
}
78. 同步访问共享的可变数据
synchronized可保证线程间互斥,可以保证共享数据写入,在每次读都能读取到最新写入的数据。
volatile可以保证线程之间的写入在其他线程可见,但不支持原子性。
利用并发包也可以支持原子操作。
当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。
79. 避免过度同步
// Broken - invokes alien method from synchronized block!
public class ObservableSet<E> extends ForwardingSet<E> {
public ObservableSet(Set<E> set) { super(set); }
private final List<SetObserver<E>> observers= new ArrayList<>();
public void addObserver(SetObserver<E> observer) {
synchronized(observers) {
observers.add(observer);
}
}
public Boolean removeObserver(SetObserver<E> observer) {
synchronized(observers) {
return observers.remove(observer);
}
}
private void notifyElementAdded(E element) {
synchronized(observers) {
for (SetObserver<E> observer : observers)
observer.added(this, element);
}
}
@Override
public Boolean add(E element) {
Boolean added = super.add(element);
if (added)
notifyElementAdded(element);
return added;
}
@Override
public Boolean addAll(Collection<? extends E> c) {
Boolean result = false;
for (E element : c)
result |= add(element);
// Calls notifyElementAdded
return result;
作者的代码这里正常(我的机器ConcurrentModificationException)
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
set.addObserver((s, e) -> System.out.println(e));
for (int i = 0; i < 100; i++)
set.add(i);
}
作者这代码会ConcurrentModificationException(一致)
set.addObserver(new SetObserver<>() {
public void added(ObservableSet<Integer> s, Integer e) {
System.out.println(e);
if (e == 23)
s.removeObserver(this);
}
});
作者这个会死锁(一样ConcurrentModificationException)
// Observer that uses a background thread needlessly
set.addObserver(new SetObserver<>() {
public void added(ObservableSet<Integer> s, Integer e) {
System.out.println(e);
if (e == 23) {
ExecutorService exec =
Executors.newSingleThreadExecutor();
try {
exec.submit(() -> s.removeObserver(this)).get();
} catch (ExecutionException | InterruptedException ex) {
throw new AssertionError (ex);
}
finally {
exec.shutdown();
}
}
}
});
总结
方法的锁不要锁住外来对象,如入参,可覆盖的方法。一方面可能会引起死锁等问题,另一方面,多核降级为单核。
80. executor 、task 和 stream 优先于线程
不仅应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程。当直接使用线程时, Thread 是既充当工作单元,又是执行机制。
fork-join 任务用 ForkJoinTask 实例表示,可以被分成更小的子任务,包含 ForkJoinPool 的线程不仅要处理这些任务,还要从另一个线程中“偷”任务,以确保所有的线程保持忙碌,从而提高 CPU 使用率、提高吞吐量,并降低延迟。