资讯专栏INFORMATION COLUMN

《Effective Java》,关于异常

CoorChice / 1864人阅读

摘要:只针对异常的情况才使用异常下面展示两种遍历数组元素的实现方式。第一种方式在访问数组边界之外的第一个数组元素时,用抛出捕获忽略异常的手段来达到终止无限循环的目的。这种做法被称为异常转译。

只针对异常的情况才使用异常

下面展示两种遍历数组元素的实现方式。

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

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

第一种方式在访问数组边界之外的第一个数组元素时,用抛出、捕获、忽略异常的手段来达到终止无限循环的目的。

第二种方式是数组循环的标准模式。

基于异常的循环模式不仅模糊了代码的意图,降低了性能,而且还不能保证正常工作。

异常应该只用于异常的情况下,不应该用于正常的控制流。
应该优先使用标准的、容易理解的模式,而不是那些声称可以提供更好性能的、弄巧成拙的方法。

设计良好的API不应该强迫客户端为了正常的控制流而使用异常。
如果类具有“状态相关”的方法,即只有特定的不可预知的条件下才可以被调用的方法,这个类往往也应该有个多带带的“状态测试”方法,即指示是否可以调用这个状态相关的方法。
例如,Iterator接口有一个“状态相关”的next方法,和相应的状态测试方法hasNext方法。

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

如果Iterator缺少hasNext方法,客户端将被迫改用下面的做法:

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

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

Java提供了三种可抛出结构:受检的异常(checked exception)、运行时异常(run-time exception)和错误(error)。

如果期望调用者能够适当地恢复,使用受检的异常。

两种未受检的可抛出结构:运行时异常和错误。
在行为上两者是等同的:都是不需要也不应该被捕获的可抛出结构。

用运行时异常表明编程错误。大多数的运行时异常都表示前提违例(precondition violation)。前提违例是指API的客户没有遵守API规范建立的约定。例如,数组访问的约定指明了数组的下标值必须在零和数组长度减1之间。ArrayIndexOutOfBoundsException表明这个前提被违反了。

最好,所有未受检的抛出结构都应该是RuntimeException的子类。

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

过分使用受检的异常会使API使用起来非常不方便。
如果方法抛出一个或者多个受检的异常,调用该方法的代码就必须在一个或者多个catch块中处理这些异常,或者声明它抛出这些异常,并让它们传播出去。

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

“把受检的异常变成未受检的异常”的一种方法是,把这个抛出异常的方法分成两个方法,其中一个方法返回一个boolean,表明是否应该抛出异常。

try {
    obj.action(args);
} catch(TheCheckedException e) {
    // Handle exceptional condition
    ...
}

重构为:

if (obj.actionPermitted(args)) {
    obj.action(args);
} else {
    // Handle exceptional condition
    ...
}

如果对象在缺少外部同步的情况下被并发访问,或者可被外界改变状态,这种重构就是不恰当的。因为在actionPermitted和action这两个调用的时间间隔之中,对象的状态有可能会发生变化。如果多带带的actionPermitted方法必须重复action方法的工作,出于性能的考虑,这种API重构就不值得去做。

优先使用标准的异常

Java平台类库提供了一组基本的未受检的异常,它们满足了绝大多数API的异常抛出需要。

重用现有的异常有多方面的好处。其中最主要的好处是,使API更加易于学习和使用,因为它与程序员已经熟悉的习惯用法是一致的。第二个好处是,可读性会更好,因为不会出现很多程序员不熟悉的异常。

常用的异常以及其使用场合:
IllegalArgumentException(非null的参数值不正确)
IllegalStateException(对于方法调用而言,对象状态不合适)
NullPointerException(在禁止使用null的情况下参数值为null)
IndexOutOfBoundsException(下标参数值越界)
ConcurrentModificationException(在禁止并发修改的情况下,检测到对象的并发修改)
UnsupportedOperationException(对象不支持用户请求的方法)

选择重用哪个异常并不总是那么精准,因为上表中的“使用场合”并不是相互排斥的。

抛出与抽象相对应的异常

如果方法抛出的异常与它所执行的的任务没有明显的关系,这种情形将会使人不知所措。

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

取自AbstractSequentialList类的异常转译例子:

/**
 * Returns the element at the specified position in this list.
 *
 * 

This implementation first gets a list iterator pointing to the * indexed element (with listIterator(index)). Then, it gets * the element using ListIterator.next 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)。如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适。

// 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);
    }
}

异常链不仅让你可以通过程序(用getCause)访问原因,还可以将原因的堆栈轨迹集成到更高层的异常中。

处理来自低层异常的最好做法是,在调用低层方法之前确保它们会成功执行,从而避免它们抛出异常。有时候,在给低层传递参数之前,检查更高层方法的参数的有效性,也能避免低层方法抛出异常。

如果无法避免低层异常,可以将异常记录下来。这样有助于调查问题,同时又将客户端代码和最终用户与问题隔离开。

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

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

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

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

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

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

当程序由于未被捕获的异常而失败的时候,系统会自动地打印该异常的堆栈轨迹。在堆栈轨迹中包含该异常的字符串表示法,即它的toString方法的调用结果。异常类型的toString方法应该尽可能多地返回有关失败原因的信息。

为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值。

为了确保在异常的细节信息中包含足够的能捕获失败的信息,一种办法是在异常的构造器中引入这些信息。例如:

/**
 * 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) {
    super("Lower bound: " + lowerBound +
          ", Upper bound: " + upperBound +
          ", index: " + index);
    this.lowerBound = lowerBound;
    this.upperBound = upperBound;
    this.index = index;
}
努力使失败保持原子性

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

有四种途径可以实现这种效果。

第一种是设计一个不可变的对象。

第二种是在执行操作之前检查参数的有效性。这可以使得在对象的状态被修改之前,先抛出适当的异常。例如,Stack.pop方法。

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

还有类似的办法是,调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。

第三种是编写一段恢复代码,来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态。这种办法主要用于永久性的数据结构。

第四种是在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。

一般,作为方法规范的一部分,产生的任何异常都应该让对象保持在该方法调用之前的状态。

不要忽略异常

当API的设计者声明一个方法将抛出某个异常的时候,等于正在试图说明某些事情。所以,请不要忽略它。

有一种情形可以忽略异常,即关闭FileInputStream的时候。因为还没有改变文件的状态,因此不必执行任何恢复动作,并且已经从文件中读取到所需要的信息,因此不必终止正在进行的操作。即使在这种情况下,把异常记录下来还是明智的做法。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70002.html

相关文章

  • Java 高效编程(Effective Java)中文第三版

    摘要:来源前条来源一书英文版已经出版,这本书的第二版想必很多人都读过,号称四大名著之一,不过第二版年出版,到现在已经将近年的时间,但随着,,,甚至的发布,语言发生了深刻的变化。译者在这里第一时间翻译成中文版。供大家学习分享之用。 来源:sjsdfg/effective-java-3rd-chinese前 51 条来源:Effective Java, Third Edition 《Effec...

    ysl_unh 评论0 收藏0
  • effective java 观后感

    摘要:这本书是我第一次买的,从买来至今整本书还没有看完,只看了一半,原因是个人比较懒,而且玩的心比较大,经过这么多年的沉淀,终于可以偷点时间写下对于这本书的观后感了整本书给我的感觉不像是一个技术书,更多的是讲解一些实用技巧,而对于我这个职场菜鸟来 effective Java 这本书是我第一次买的, 从买来至今整本书还没有看完, 只看了一半, 原因是个人比较懒,而且玩的心比较大,经过这么多年...

    calx 评论0 收藏0
  • Effective Java 第三版 全文翻译

    摘要:本章中的大部分内容适用于构造函数和方法。第项其他方法优先于序列化第项谨慎地实现接口第项考虑使用自定义的序列化形式第项保护性地编写方法第项对于实例控制,枚举类型优先于第项考虑用序列化代理代替序列化实例附录与第版中项目的对应关系参考文献 effective-java-third-edition 介绍 Effective Java 第三版全文翻译,纯属个人业余翻译,不合理的地方,望指正,感激...

    galois 评论0 收藏0
  • Effective Java 3rd.Edition 翻译

    摘要:推荐序前言致谢第一章引言第二章创建和销毁对象第项用静态工厂方法代替构造器第项遇到多个构造器参数时要考虑使用构建器第项用私有构造器或者枚举类型强化属性第项通过私有构造器强化不可实例化的能力第项优先考虑依赖注入来引用资源第项避免创建不必要的对象 推荐序 前言 致谢 第一章 引言 第二章 创建和销毁对象 第1项:用静态工厂方法代替构造器 第2项:遇到多个构造器参数时要考虑使用构建器 第...

    KoreyLee 评论0 收藏0
  • Effective Java 笔记

    摘要:如果采用抽象类,则属性组合可能导致子类的组合爆炸。内部类的设计静态成员类用修饰的内部类,可以不依赖于外部实例进行创建。如下所示其构造函数默认是,并且无法修改。 对所有对象都通用的方法 equals和hashCode方法的关系 重写equals方法必须也要重写hashCode方法。 equals用的属性没变,则多次调用hashCode返回值也必须保持不变。 equals比较相等的对象,...

    enali 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<