pursue wind pursue wind
首页
Java
Python
数据库
框架
Linux
中间件
前端
计算机基础
DevOps
项目
面试
书
关于
归档
MacOS🤣 (opens new window)
GitHub (opens new window)
首页
Java
Python
数据库
框架
Linux
中间件
前端
计算机基础
DevOps
项目
面试
书
关于
归档
MacOS🤣 (opens new window)
GitHub (opens new window)
  • EffectiveJava

    • 考虑使用静态工厂方法替代构造方法
    • 当构造方法参数过多时使用builder模式
    • 使用私有构造方法或枚类实现Singleton属性
    • 使用私有构造方法执行非实例化
    • 依赖注入优于硬连接资源(hardwiring resources)
    • 避免创建不必要的对象
    • 消除过期的对象引用
    • 避免使用Finalizer和Cleaner机制
    • 使用try-with-resources语句替代try-finally语句
    • 重写equals方法时遵守通用约定
    • 重写equals方法时同时也要重写hashcode方法
    • 始终重写 toString 方法
    • 谨慎地重写 clone 方法
    • 考虑实现Comparable接口
    • 使类和成员的可访问性最小化
    • 在公共类中使用访问方法而不是公共属性
    • 最小化可变性
    • 组合优于继承
    • 要么设计继承并提供文档说明,要么禁用继承
    • 接口优于抽象类
    • 为后代设计接口
    • 接口仅用来定义类型
    • 类层次结构优于标签类
    • 支持使用静态成员类而不是非静态类
    • 将源文件限制为单个顶级类
    • 不要使用原始类型
    • 消除非检查警告
    • 列表优于数组
    • 优先考虑泛型
    • 优先使用泛型方法
    • 使用限定通配符来增加API的灵活性
    • 合理地结合泛型和可变参数
    • 优先考虑类型安全的异构容器
    • 使用枚举类型替代整型常量
    • 使用实例属性替代序数
    • 使用EnumSet替代位属性
    • 使用EnumMap替代序数索引
    • 使用接口模拟可扩展的枚举
    • 注解优于命名模式
    • 始终使用Override注解
    • 使用标记接口定义类型
    • lambda表达式优于匿名类
    • 方法引用优于lambda表达式
    • 优先使用标准的函数式接口
    • 明智审慎地使用Stream
    • 优先考虑流中无副作用的函数
    • 优先使用Collection而不是Stream来作为方法的返回类型
    • 谨慎使用流并行
    • 检查参数有效性
    • 必要时进行防御性拷贝
    • 仔细设计方法签名
    • 明智审慎地使用重载
    • 明智审慎地使用可变参数
    • 返回空的数组或集合,不要返回 null
    • 明智审慎地返回 Optional
    • 为所有已公开的 API 元素编写文档注释
    • 最小化局部变量的作用域
    • for-each 循环优于传统 for 循环
    • 了解并使用库
    • 若需要精确答案就应避免使用 float 和 double 类型
    • 基本数据类型优于包装类
    • 当使用其他类型更合适时应避免使用字符串
    • 当心字符串连接引起的性能问题
    • 通过接口引用对象
    • 接口优于反射
    • 明智审慎地本地方法
    • 明智审慎地进行优化
    • 遵守被广泛认可的命名约定
    • 只针对异常的情况下才使用异常
    • 对可恢复的情况使用受检异常,对编程错误使用运行时异常
    • 避免不必要的使用受检异常
    • 优先使用标准的异常
    • 抛出与抽象对应的异常
    • 每个方法抛出的异常都需要创建文档
    • 在细节消息中包含失败一捕获信息
    • 保持失败原子性
    • 不要忽略异常
    • 同步访问共享的可变数据
    • 避免过度同步
    • executor 、task 和 stream 优先于线程
    • 并发工具优于 wait 和 notify
    • 文档应包含线程安全属性
    • 明智审慎的使用延迟初始化
    • 不要依赖线程调度器
    • 优先选择 Java 序列化的替代方案
    • 非常谨慎地实现 Serializable
    • 考虑使用自定义的序列化形式
    • 保护性的编写 readObject 方法
    • 对于实例控制,枚举类型优于 readResolve
    • 考虑用序列化代理代替序列化实例
  • On Java 8

  • 书
  • EffectiveJava
pursuewind
2020-11-22

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

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

条目 18 中提醒你注意继承没有设计和文档说明的「外来」类的子类化的危险。 那么对于专门为了继承而设计并且具有良好文档说明的类而言,这又意味着什么呢?

首先,这个类必须准确地描述重写每个方法带来的影响。 换句话说,该类必须文档说明可重写方法的自用性(self-use)。 对于每个 public 或者 protected 的方法,文档必须指明方法调用哪些可重写方法,以何种顺序调用的,以及每次调用的结果又是如何影响后续处理。 (重写方法,这里是指非 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.
1
2
3
4
5

从该集合中删除指定元素的单个实例(如果存在,optional 实例操作)。 更广义地说,如果这个集合包含一个或多个这样的元素 e,就删除其中的一个满足 Objects.equals(o, e) 的元素 e。 如果此集合包含指定的元素(或者等同于此集合因调用而发生了更改),则返回 true。

实现要求: 这个实现迭代遍历集合查找指定元素。 如果找到元素,则使用迭代器的 remove 方法从集合中删除元素。 请注意,如果此集合的 iterator 方法返回的迭代器未实现 remove 方法,并且此集合包含指定的对象,则该实现将引发 UnsupportedOperationException 异常。

这个文档清楚地说明,重写 iterator 方法将会影响 remove 方法的行为。 它还描述了 iterator 方法返回的 Iterator 行为将如何影响 remove 方法的行为。 与条目 18 中的情况相反,在这种情况下,程序员继承 HashSet 并不能说明重写 add 方法是否会影响 addAll 方法的行为。

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

@implSpec 标签是在 Java 8 中添加的,并且在 Java 9 中被大量使用。这个标签应该默认启用,但是从 Java 9 开始,除非通过命令行开关-tag "apiNote:a:API Note:",否则 Javadoc 工具仍然会忽略它。

为了继承而进行的设计不仅仅涉及自用模式的文档设计。为了使程序员能够编写出更加有效的子类,而无须承受不必要的痛苦,类必须以精心挑选的 protected 方法的形式,提供适当的钩子(hook),以便进入其内部工作中。或者在罕见的情况下,提供受保护的属性。 例如,考虑 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 operation has 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.nextfollowed 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.
1
2
3
4
5
6
7
8
9
10
11

从此列表中删除索引介于 fromIndex(包含)和 inclusive(不含)之间的所有元素。 将任何后续元素向左移(减少索引)。 这个调用通过(toIndex - fromIndex)元素来缩短列表。 (如果 toIndex == fromIndex,则此操作无效。)

这个方法是通过列表及其子类的 clear 操作来调用的。重写这个方法利用列表内部实现的优势,可以大大提高列表和子类的 clear 操作性能。

实现要求: 这个实现获取一个列表迭代器,它位于 fromIndex 之前,并重复调用 ListIterator.remove 和 ListIterator.next 方法,直到整个范围被删除。 注意:如果 ListIterator.remove 需要线性时间,则此实现需要平方级时间。

参数:
  fromIndex 要移除的第一个元素的索引
  toIndex 要移除的最后一个元素之后的索引

这个方法对 List 实现的最终用户来说是没有意义的。 它仅仅是为了使子类很容易提供一个快速 clear 方法。 在没有 removeRange 方法的情况下,当在子列表上调用 clear 方法,子类将不得不使用平方级的时间,否则,或从头重写整个 subList 机制——这不是一件容易的事情!

那么当你设计一个继承类的时候,你如何决定暴露哪些的受保护的成员呢? 不幸的是,没有灵丹妙药。 所能做的最好的就是努力思考,做出最好的测试,然后通过编写子类来进行测试。 应该尽可能少地暴露受保护的成员,因为每个成员都表示对实现细节的承诺。 另一方面,你不能暴露太少,因为失去了保护的成员会导致一个类几乎不能用于继承。

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

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

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

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

public class Super {
    // Broken - constructor invokes an overridable method
    public Super() {
        overrideMe();
    }
    public void overrideMe() {
    }
}
1
2
3
4
5
6
7
8

以下是一个重写 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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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

请注意,从构造方法中调用私有方法,其中任何一个方法都不可重写的,那么 final 方法和静态方法是安全的。

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

如果你决定在为继承而设计的类中实现 Cloneable 或 Serializable 接口,那么应该知道,由于 clone 和 readObject 方法与构造方法相似,所以也有类似的限制: clone 和 readObject 都不会直接或间接调用可重写的方法。 在 readObject 的情况下,重写方法将在子类的状态被反序列化之前运行。 在 clone 的情况下,重写方法将在子类的 clone 方法有机会修复克隆的状态之前运行。 在任何一种情况下,都可能会出现程序故障。 在 clone 的情况下,故障可能会损坏原始对象以及被克隆对象本身。 例如,如果重写方法假定它正在修改对象的深层结构的拷贝,但是尚未创建拷贝,则可能发生这种情况。

最后,如果你决定在为继承设计的类中实现 Serializable 接口,并且该类有一个 readResolve 或 writeReplace 方法,则必须使 readResolve 或 writeReplace 方法设置为受保护而不是私有。 如果这些方法是私有的,它们将被子类无声地忽略。 这是另一种情况,把实现细节成为类的 API 的一部分,以允许继承。

到目前为止,设计一个继承类需要很大的努力,并且对这个类有很大的限制。 这不是一个轻率的决定。 有些情况显然是正确的,比如抽象类,包括接口的骨架实现(skeletal implementations)(详见第 20 条)。 还有其他的情况显然是错误的,比如不可变的类(详见第 17 条)。

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

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

这个建议可能有些争议,因为许多程序员已经习惯于继承普通的具体类来增加功能,例如通知和同步等功能,或限制原有类的功能。 如果一个类实现了捕获其本质的一些接口,比如 Set,List 或 Map,那么不应该为了禁止子类化而感到愧疚。 在条目 18 中描述的包装类模式为增强功能提供了继承的优越选择。

如果一个具体的类没有实现一个标准的接口,那么你禁止继承可能给一些程序员带来不便。 如果你觉得你必须允许从这样的类继承,一个合理的方法是确保类从不调用任何可重写的方法,并文档说明这个事实。 换句话说,完全消除类的自用(self-use)的可重写的方法。 这样做,你将创建一个合理安全的子类。 重写一个方法不会影响任何其他方法的行为。

你可以机械地消除类的自我使用的重写方法,而不会改变其行为。 将每个可重写的方法的主体移动到一个私有的“帮助器方法”,并让每个可重写的方法调用其私有的帮助器方法。 然后用直接调用可重写方法的专用帮助器方法来替换每个自用的可重写方法。

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

Last Updated: 2023/01/30, 11:01:00
组合优于继承
接口优于抽象类

← 组合优于继承 接口优于抽象类→

Theme by Vdoing | Copyright © 2019-2023 pursue-wind | 粤ICP备2022093130号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
  • 飙升榜
  • 新歌榜
  • 云音乐民谣榜
  • 美国Billboard榜
  • UK排行榜周榜
  • 网络DJ