2
0
mirror of https://github.com/Jueee/effective-Java.git synced 2025-03-14 19:30:27 +08:00
effective-Java/ch04类和接口/19.要么设计继承并提供文档说明,要么禁用继承.md
2019-10-24 20:11:14 +08:00

4.3 KiB
Raw Blame History

要么设计继承并提供文档说明,要么禁用继承

对于专门为了继承而设计并且具有良好文档说明的类而言,这个类必须准确地描述重写每个方法带来的影响。

换句话说该类必须文档说明可重写方法的自用性self-use。 对于每个 public 或者 protected 的方法,文档必须指明方法调用哪些可重写方法,以何种顺序调用的,以及每次调用的结果又是如何影响后续处理。更一般地说,一个类必须文档说明任何可能调用可重写方法的情况。

关于程序文档有句格言:好的 API 文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。

为了继承而进行的设计不仅仅涉及自用模式的文档设计。为了使程序员能够编写出更加有效的子类,而无须承受不必要的痛苦,类必须以精心挑选的 protected 方法的形式提供适当的钩子hook以便进入其内部工作中。或者在罕见的情况下,提供受保护的属性。

测试为继承而设计的类的唯一方法是编写子类。 如果你忽略了一个关键的受保护的成员,试图编写一个子类将会使得遗漏痛苦地变得明显。 相反,如果编写的几个子类,而且没有一个使用受保护的成员,那么应该将其设为私有。 经验表明,三个子类通常足以测试一个可继承的类。 这些子类应该由父类作者以外的人编写。

当你为继承设计一个可能被广泛使用的类的时候,要意识到你永远承诺你文档说明的自用模式以及隐含在其保护的方法和属性中的实现决定。 这些承诺可能会使后续版本中改善类的性能或功能变得困难或不可能。 因此, 在发布它之前,你必须通过编写子类来测试你的类。

另外,请注意,继承所需的特殊文档混乱了正常的文档,这是为创建类的实例并在其上调用方法的程序员设计的。 在撰写本文时,几乎没有工具将普通的 API 文档从和仅仅针对子类实现的信息,分离出来。

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

  • Item19Example01.java:违反这个规则的类。

    期望这个程序打印两次 instant 实例,但是它第一次打印出 null,因为在 Sub 构造方法有机会初始化 instant 属性之前,overrideMeSuper 构造方法调用。

    请注意,这个程序观察两个不同状态的 final 属性! 还要注意的是,如果 overrideMe 方法调用了 instant 实例中任何方法,那么当父类构造方法调用 overrideMe 时,它将抛出一个 NullPointerException 异常。

    这个程序不会抛出 NullPointerException 的唯一原因是 println 方法容忍 null 参数。

在修改非 final 的具体类的内部之后,接收与子类相关的错误报告并不少见,这些类没有为继承而设计和文档说明。解决这个问题的最好办法是,在没有想要安全地子类化的设计和文档说明的类中禁止子类化。 有两种方法禁止子类化。 两者中较容易的是声明类为 final。 另一种方法是使所有的构造方法都是私有的或包级私有的,并且添加公共静态工厂来代替构造方法。

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