Effective Java 第三版读书笔记——条款19:为继承设计并文档说明,否则不该使用继承

条款 18 提醒你注意没有设计和文档说明(针对继承)的“外来”类的子类化的危险。 那么为了继承而设计和文档说明一个类是什么意思呢?

首先,这个类必须准确地描述重写这个方法带来的影响。换句话说,该类必须文档说明可重写方法的自用性(self-use)。对于每个公共或受保护的方法,文档必须指明方法调用哪些重写方法,以何种顺序以及每次调用的结果如何影响后续处理。 (重写方法,这里是指非 final 修饰的方法,无论是公开还是保护的)更一般地说,一个类必须文档说明任何可能调用可重写方法的情况。例如,后台线程或者静态初始化代码块可能会调用这样的方法。

调用可重写方法的方法在文档注释结束时应该包含对这些调用的描述。这些描述是规范中的特殊部分,标记为“Implementation Requirements”,由 Javadoc 标签 @implSpec 生成。这个部分介绍该方法的内部工作原理。下面是从 java.util.AbstractCollection 类的规范中拷贝的例子:

public boolean remove(Object o)
	Removes a single instance of the specified element from this collection, if it is present (optional operation). More formally, removes an element e such that Objects.equals(o, e), if this collection contains one or more such elements. Returns true if this collection contained the specified element (or equivalently, if this collection changed as a result of the call).
	Implementation Requirements: This implementation iterates over the collection looking for the specified element. If it finds the element, it removes the element from the collection using the iterator’s remove method. Note that this implementation throws an UnsupportedOperationException if the iterator returned by this collection’s iterator method does not implement the remove method and this collection contains the specified object.

这个文档毫无疑问地说明,重写 iterator 方法会影响 remove 方法的行为。它还描述了 iterator 方法返回的 Iterator 行为将如何影响 remove 方法的行为。但是,这是否违背了一个良好的 API 文档应该描述给定的方法是什么,而不是它是如何做的呢?是的,它确实!这是继承违反封装这一事实的不幸后果。

设计继承涉及的不仅仅是文档说明自用的模式。为了让程序员能够写出有效的子类而不会带来不适当的痛苦,一个类可能以明智选择的受保护方法的形式提供内部工作,或者在罕见的情况下,提供受保护的属性。例如,考虑 java.util.AbstractList 中的 removeRange 方法:

protected void removeRange(int fromIndex, int toIndex)
	Removes from this list all of the elements whose index is between fromIndex, inclusive, and toIndex, exclusive. Shifts any succeeding elements to the left (reduces their index). This call shortens the list by(toIndex - fromIndex) elements. (If toIndex == fromIndex, this operationhas no effect.)
	This method is called by the clear operation on this list and its sublists. Overriding this method to take advantage of the internals of the list implementation can substantially improve the performance of the clear operation on this list and its sublists.
	Implementation Requirements: This implementation gets a list iterator positioned before fromIndex and repeatedly calls ListIterator.next followed by ListIterator.remove, until the entire range has been removed. Note: If ListIterator.remove requires linear time, this implementation requires quadratic time.
Parameters:
	fromIndex index of first element to be removed.
	toIndex index after last element to be removed.

这个方法对 List 实现的最终用户来说是没有意义的。它仅仅是为了使子类更容易提供一个快速 clear 方法。在没有 removeRange 方法的情况下,当在子列表上调用 clear 方法,子类将不得不使用平方级的时间。

测试为继承而设计的类的唯一方法是编写子类。如果你忽略了一个关键的受保护的成员,试图编写一个子类将会使遗漏的痛苦变得更明显。经验表明,三个子类通常足以测试一个可继承的类。

还有一些允许继承的类必须遵守的限制。构造方法绝不能直接或间接调用可重写的方法。如果违反这个规则,将导致程序失败。父类构造方法在子类构造方法之前运行,所以在子类构造方法运行之前,子类中的重写方法被调用。如果重写方法依赖于子类构造方法执行的任何初始化,则此方法将不会按预期运行。为了具体说明,这是一个违反这个规则的类:

public class Super {
    // Broken - constructor invokes an overridable method
    public Super() {
        overrideMe();
    }
    public void overrideMe() {
    }
}

以下是一个重写 overrideMe 方法的子类,Super 类的唯一构造方法会错误地调用它:

public final class Sub extends Super {
    // Blank final, set by constructor
    private final Instant instant;

    Sub() {
        instant = Instant.now();
    }

    // Overriding method invoked by superclass constructor
    @Override public void overrideMe() {
        System.out.println(instant);
    }

    public static void main(String[] args) {
        Sub sub = new Sub();
        sub.overrideMe();
    }
}

你可能期望这个程序打印两次 instant 实例,但是它第一次打印出 null,因为在 Sub 构造方法有机会初始化 instant 属性之前,overrideMe 首先被 Super 构造方法调用。请注意,这个程序观察两个不同状态的 final 属性!还要注意的是,如果 overrideMe 方法调用了 instant 实例中的任何方法,那么当父类构造方法调用 overrideMe 时,它将抛出一个 NullPointerException 异常。这个程序不会抛出 NullPointerException 的唯一原因是 println 方法容忍 null 参数。

CloneableSerializable 接口在设计继承时会带来特殊的困难。对于为继承而设计的类来说,实现这些接口通常不是一个好主意,因为这会给继承类的程序员带来很大的负担。然而,可以采取特殊的行动来允许子类实现这些接口,而不需要强制这样做。这些操作在条款 13 和条款 86 中有描述。

到目前为止,设计一个继承类需要很大的努力,并且对这个类有很大的限制。但是普通的具体类呢?传统上,它们既不是 final 的,也不是为了子类化而设计和文档说明的,但是这种情况是危险的。每次修改这样的类,则继承此类的子类将被破坏。这不仅仅是一个理论问题。 在修改非 final 的具体类的内部之后,接收与子类相关的错误报告并不少见,这些类没有为继承而设计和文档说明。

解决这个问题的最好办法是,在没有想要安全地子类化的设计和文档说明的类中禁止子类化。有两种方法禁止子类化。两者中较容易的是声明类为 final。另一种方法是使所有的构造方法都是私有的或包级私有的,并且添加公共静态工厂来代替构造方法。这个方案在内部提供了使用子类的灵活性,在条款 17 中讨论过。两种方法都是可以接受的。

总之,设计一个继承类是一件很辛苦的事情。你必须文档说明所有的自用模式,一旦你文档说明了它们,必须承诺为他们的整个生命周期负责。如果你不这样做,子类可能会依赖于父类的实现细节,并且如果父类的实现发生改变,子类可能会损坏。为了允许其他人编写高效的子类,可能还需要导出一个或多个受保护的方法。

猜你喜欢

转载自blog.csdn.net/sky_asd/article/details/86560931
今日推荐