Skip to content

Latest commit

 

History

History
147 lines (109 loc) · 14.4 KB

第19条:要么设计继承并提供文档说明,要么禁止继承 44ebcb5abfc3446e886df1d220e5154e.md

File metadata and controls

147 lines (109 loc) · 14.4 KB

第19条:要么设计继承并提供文档说明,要么禁止继承

第18条提醒我们:对于不是为了继承而设计并且没有文档说明的“外来”类进行子类化是多么危险的。那么对于专门为了设计继承而设计并且具有良好文档说明的类而言,这又意味着什么呢?

首先,该类的文档必须精确的描述覆盖每个方法所带来的影响。换句话说,该类必须有文档说明它可覆盖的方法的自用性。对于每个共有的或则受保护的方法或则构造器,他的文档必须指明该方法或则构造器调用了哪些可覆盖的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续处理过程的(所谓覆盖的方法,是指非final的、公有的或受保护的)。更广义地说,即类必须在文档中说明,在那些情况下会调用可覆盖地方法。例如,后台的线程或者静态的初始化器,可能会调用这样的方法。

如果方法调用到了可覆盖的方法,在它的文档注释的末尾应该包含关于这些调用的描述信息。这段描述信息是规范的一个特殊部分,写着:“Implementation,Requirements”,它由Javadoc标签@implSpec生成。这段话描述了该方法的内部工作情况。下面举个例子,摘自java.util.AbstractCollection 规范:

/**
     * {@inheritDoc}
     *
     * @implSpec
     * 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.
     *
     * <p>Note that this implementation throws an
     * {@code UnsupportedOperationException} if the iterator returned by this
     * collection's iterator method does not implement the {@code remove}
     * method and this collection contains the specified object.
     *
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws ClassCastException            {@inheritDoc}
     * @throws NullPointerException          {@inheritDoc}
     */
    public boolean remove(Object o) {
			...
    }

(如果这个集合中存在指定的元素,就从中删除指定元素中的单个实例(这是项的可选操作)。更广义地说,即如果集合中包含一个或则多个这样地元素e,就从中删除掉一个,如Objects.equals(o,e)。如果集合中包含指定的元素,就放回true(如果调用的结果改变了集合,也是一样)。实现要求:该实现遍历了整个集合来查找指定的元素。如果它找到该元素,将会利用迭代器的remove方法将值从集合中删除。注意,如果由该集合的iterator方法返回的迭代器没有实现remove方法,该实现就会抛出UnsupportedOperationException异常。)

这份文档清楚的说明了,覆盖iterator方法将会影响remove方法的行为。而且,它确切的描述了iterator方法返回的Iterator的行为将会怎样影响remove方法的行为,与此相反的是,在第18条的情形中,程序员在子类化HashSet的时候,无法说明覆盖add方法是否会影响addAll方法的行为。

关于程序文档有句格言:好的API文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。那么上面这种做法是否违背了这句格言呢?是的,它确实违背了!,这正是继承破坏了封装性所带来的不幸的后果。所以,为了设计一个类的文档,以便它们能够安全的子类化,你必须描述清楚那些可能未定义的实现细节。

@implSpec标签是在Java 8中增加的,在Java 9中得到了广泛的应用。这个标签应该是默认可用的,但是到Java 9,Javadoc工具仍然把它忽略,除非传入命令行参数: -tag"apiNote:a:API Note:"

为了继承而进行的设计不仅仅设计自用模式的文档设计。为了使程序员能够编写出更加有效的子类,而无需承受不必要的痛苦,类必须以精心挑选的受保护的方法的形式,提供适当的钩子,以便进入其内部工作中。这种形式也可以是罕见的实例,或则受保护的域。例如,以java.util.AbstractList中的removeRange方法为例:

/**
 * Removes from this list all of the elements whose index is between
 * {@code fromIndex}, inclusive, and {@code toIndex}, exclusive.
 * Shifts any succeeding elements to the left (reduces their index).
 * This call shortens the list by {@code (toIndex - fromIndex)} elements.
 * (If {@code toIndex==fromIndex}, this operation has no effect.)
 *
 * <p>This method is called by the {@code clear} operation on this list
 * and its subLists.  Overriding this method to take advantage of
 * the internals of the list implementation can <i>substantially</i>
 * improve the performance of the {@code clear} operation on this list
 * and its subLists.
 *
 * @implSpec
 * This implementation gets a list iterator positioned before
 * {@code fromIndex}, and repeatedly calls {@code ListIterator.next}
 * followed by {@code ListIterator.remove} until the entire range has
 * been removed.  <b>Note: if {@code ListIterator.remove} requires linear
 * time, this implementation requires quadratic time.</b>
 *
 * @param fromIndex index of first element to be removed
 * @param toIndex index after last element to be removed
 */
protected void removeRange(int fromIndex, int toIndex) {
      ListIterator<E> it = listIterator(fromIndex);
      for (int i=0, n=toIndex-fromIndex; i<n; i++) {
          it.next();
          it.remove();
      }
  }

(从列表中删除索引处于fromIndex(含)和toIndex(不含)之间的元素。所有符合条件的元素移到左边(减小它们索引)。这一调用将从ArraysList中删除从toIndexfromIndex的元素。(如果toIndex == fromIndex,这项操作就无效。)这个方法是通过clear操作这个列表及其子列表中调用的。覆盖这个方法来利用列表实现的内部信息,可以充分的改善这个列表及其子列表中的clear操作的性能。实现要求:这项实现获得了一个处在fromIndex之前的列表迭代器,并依次重复的调用ListIterator.nextListIterator.remove,直到整个范围都被移除为止。注意:如果ListIntertor.remove需要线性的时间,该实现就需要平方级的时间。

参数:

fromIndex要移除的第一个元素的索引。

toIndex要移除的最后一个元素之后的索引。)

这个方法对于List实现的最终用户并没有意义。提供该方法的唯一目的在于,使子类更易于提供针对子列表的快速clear方法。如果没有removeRanger方法,当在子列表上调用clear方法时,子类将不得不用平方级的时间来完成它的工作。否则就得重新编写这个subList机制——这可不是一件容易的事。

因此,当你为了继承而设计类的时候,,如何决定应该暴露那些受保护的方法或则域呢?遗憾的是,并没有什么神奇的法则可供你使用。你所能做到的最佳路径就是努力思考,发挥最好的想象,然后编写一些子类进行测试。你应该尽可能地少暴露受保护地成员,因为每个方法或则域都代表了一项关于实现细节地承诺。另一方面,你又不能暴露地太少,因为漏掉的受保护方法会导致这个类无法被真正用于继承。

对于为了继承而设计的类,唯一的测试方法就是编写测试子类。如果遗漏了关键的受保护的成员,尝试编写子类就会使遗憾所带来的痛苦变得更加明显。相反,如果编写了多个子类,并且无一使用受保护的成员,或许就应该把它做成私有的。经验表明,3个子类通常就足以而是一个可扩展的类。处理超类的程序设计者之外,都需要编写一个或则多个这样的子类。

为了继承而设计的有可能被广泛使用的类时,就必须要意识到,对于文档中说明的自用模式,以及对于其受保护的方法和域中所隐含的实现策略,你实际上已经做出了永久的承诺。这些承诺是的你在后续的版本中提高这个类的性能或则增加新功能都变得非常困难,甚至不可能。因此,必须在发布类之前先编写子类对类进行测试。

为了允许继承,类还必须遵守其它的一些约束。构造器绝不能调用可被覆盖的方法,无论是直接调用还是间接调用。如果违反了这条规则,很有可能导致程序失败。超类的构造器在子类的构造器之前运行,所以,子类中覆盖版本的方法将会在子类的构造器运行之前先被调用。如果该覆盖版本的方法依赖于子类构造器所执行的任何初始化工作,该方法将不会如预期般执行。为了更加直观地说明这一点,下面举个例子,其中有个违反了这条规则:

public class Super {
    public Super(){
        overrideMe();
    }

    public void overrideMe(){

    }
}

下面地子类覆盖了方法overrideMeSuper唯一地构造器就错误地调用了这个方法:

public class Sub extends Super{
    private final Instant instant;

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

    @Override
    public void overrideMe() {
        System.out.println(instant);
    }

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

你可能会期待打印两次日期,但是它第一次打印出地是null,因为overrideMe方法被Super构造器调用地时候,构造器Sub还没有机会初始化instant域。注意,这个程序观察到的final域处于两种不同的状态。还要注意的是,如果overrideMe已经调用了instant中的任何方法,当Super构造器调用overrideMe的时候,调用就会抛出NullPointException异常。如果程序没有抛出NullPointException异常,唯一的原因在于println方法可以容忍null参数。

注意,通过构造器调用私有的方法、final方法和静态方法是安全的,这些都不是可以被覆盖的方法。

在为了继承而设计类的时候,CloneableSerializable接口出现了特殊的困难。如果类是为了继承而设计的,无论实现这其中的那个接口通常都不是个好的注意,因为它们把一些实质性的负担转嫁到了扩展这个类的程序员身上。然而,你还是可以采取一些特殊的手段,允许子类实现这些接口,无需强迫子类的程序员去承受这些负担。

如果你决定在一个为了继承而设计的类中实现CloneableSerializable接口,就应该意识到,因为clonereadObejct方法在行为上非常类似于构造器,所以类似的限制也是适用的:无论是clone还是readObject,都不可以调用覆盖的方法,不管是直接还是间接方式。对于readObejct方法,覆盖的方法将在子类的状态被反序列化之前被运行:而对于clone方法,覆盖的方法则是在子类的clone方法有机会修正被克隆对象的状态之前被运行。无论哪种情形,都不可避免地将导致程序的失败,在clone方法的情形中,这种失败可能会同时损害到原始的对像以及被克隆的对象本身。例如,如果覆盖版本的方法假设它正在修正改对象深层次结构的克隆对象的备份,就会发生这种情况,但是备份还没有完成。

最后,如果你决定在一个为了继承而设计的类中实现Serializable接口,并且该类有一个readResolve或则writeReplace方法,就必须使用readResolve或则writeReplace成为受保护的方法,而不是私有的方法。如果这些方法是私有的,那么子类将会不声不响的忽略掉这两个方法。这正是“为了允许继承,而把实现细节变成一个类的API的一部分”的另一种情形。

到现在为止,结论应该很明显了:为了继承而设计的类,对于这个类会有一些实质性的限制。这并不是很轻松就可以承诺的决定。在某些情况下,这一的决定很明显是正确的,比如抽象类,包括接口的骨架实现。但是,在另一些情况下,这一的决定却很明显是错误的,比如不可变类。

但是,对于普通的具体类应该怎么办呢?它们既不是final的,也不是为了子类化而设计和编写文档的,所以这种状况很危险。每次对这种类进行修改,从这个类扩展得到的客户就有可能遭到破坏。这不仅仅是一个理论问题。对于一个并非为了继承而设计的非final具体类,在修改了它的内部实现之后,接收到于子类化相关的错误报告也并不少见。

这个问题的最佳解决方案是,对于那些并非为了安全地进行子类化而设计和编写地文档,,要禁止子类化。有两种方法禁止子类化。比较容易地方法是把这个类声明未final的。另一种方法是吧所有的构造器都变成私有的,或则包级私有的,并增加一些公有的静态工厂在替代构造器。

如果具体的类没有实现标准的接口,那么禁止继承可能会给某些程序员带来不便,如果你认为必须允许继承这样的类继承,一种合理的办法是确保这个类永远不会调用它的任何覆盖的方法,并在文档中说明这一点。换句话说,完全消除这个类中可覆盖的方法的自用特性。这样做之后,就可以创建”能够安全的进行子类化“的类。覆盖方法将永远不会影响其他任何方法的行为。

你可以机械的消除类中可覆盖方法的自用性,而不改变它的行为。将每个可覆盖的方法的代码移到一个私有的”辅助方法“中,并且让每个可覆盖的方法调用它的私有辅助方法。然后用”直接调用可覆盖方法的私有辅助方法“来代替”可覆盖方法的每个自用调用“。

简而言之,专门为了继承而设计类是一件很辛苦的工作。你必须建立文档说明其所有的自用模式,并且一旦建立文档,在这个文档的整个声明周期中都必须遵守。如果没有做到,子类就会依赖超类的实现细节,如果超类的实现细节发生了变化,它就有可能遭到破坏。为了允许他人能够编写出高效的子类,你还必须导出一个或则多个受保护的方法。除非你知道真正需要子类,否则只好禁止类被继承。