|
1 |
| - 为后代设计接口 |
| 1 | +20 为后代设计接口 |
| 2 | + |
2 | 3 | 在 Java 8 之前,不可能在不破坏现有实现的情况下为接口添加方法。 如果向接口添加了一个新方法,现有的实现通常会缺少该方法,从而导致编译时错误。 在 Java 8 中,添加了默认方法(default method)构造[JLS 9.4],目的是允许将方法添加到现有的接口。 但是增加新的方法到现有的接口是充满风险的。
|
3 | 4 |
|
4 | 5 | 默认方法的声明包含一个默认实现,该方法允许实现接口的类直接使用,而不必实现默认方法。 虽然在 Java 中添加默认方法可以将方法添加到现有接口,但不能保证这些方法可以在所有已有的实现中使用。 默认的方法被「注入(injected)」到现有的实现中,没有经过实现类的知道或同意。 在 Java 8 之前,这些实现是用默认的接口编写的,它们的接口永远不会获得任何新的方法。
|
@@ -34,3 +35,135 @@ default boolean removeIf(Predicate<? super E> filter) {
|
34 | 35 | 准则是清楚的。 尽管默认方法现在是 Java 平台的一部分,但是非常悉心地设计接口仍然是非常重要的。 虽然默认方法可以将方法添加到现有的接口,但这样做有很大的风险。 如果一个接口包含一个小缺陷,可能会永远惹怒用户。 如果一个接口严重缺陷,可能会破坏包含它的 API。
|
35 | 36 |
|
36 | 37 | 因此,在发布之前测试每个新接口是非常重要的。 多个程序员应该以不同的方式实现每个接口。 至少,你应该准备三种不同的实现。 编写多个使用每个新接口的实例来执行各种任务的客户端程序同样重要。 这将大大确保每个接口都能满足其所有的预期用途。 这些步骤将允许你在发布之前发现接口中的缺陷,但仍然可以轻松地修正它们。 虽然在接口被发布后可能会修正一些存在的缺陷,但不要太指望这一点。
|
| 38 | + |
| 39 | +22. 接口仅用来定义类型 |
| 40 | + 当类实现接口时,该接口作为一种类型(type),可以用来引用类的实例。因此,一个类实现了一个接口,因此表明客户端可以如何处理类的实例。为其他目的定义接口是不合适的。 |
| 41 | + |
| 42 | + 一种失败的接口就是所谓的常量接口(constant interface)。 这样的接口不包含任何方法; 它只包含静态 final 属性,每个输出一个常量。 使用这些常量的类实现接口,以避免需要用类名限定常量名。 这里是一个例子: |
| 43 | + |
| 44 | +// Constant interface antipattern - do not use! |
| 45 | +public interface PhysicalConstants { |
| 46 | + // Avogadro's number (1/mol) |
| 47 | + static final double AVOGADROS_NUMBER = 6.022_140_857e23; |
| 48 | + |
| 49 | + // Boltzmann constant (J/K) |
| 50 | + static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23; |
| 51 | + |
| 52 | + // Mass of the electron (kg) |
| 53 | + static final double ELECTRON_MASS = 9.109_383_56e-31; |
| 54 | +} |
| 55 | + 常量接口模式是对接口的糟糕使用。 类在内部使用一些常量,完全属于实现细节。实现一个常量接口会导致这个实现细节泄漏到类的导出 API 中。对类的用户来说,类实现一个常量接口是没有意义的。事实上,它甚至可能使他们感到困惑。更糟糕的是,它代表了一个承诺:如果在将来的版本中修改了类,不再需要使用常量,那么它仍然必须实现接口,以确保二进制兼容性。如果一个非 final 类实现了常量接口,那么它的所有子类的命名空间都会被接口中的常量所污染。 |
| 56 | + |
| 57 | + Java 平台类库中有多个常量接口,如 java.io.ObjectStreamConstants。 这些接口应该被视为不规范的,不应该被效仿。 |
| 58 | + |
| 59 | + 如果你想导出常量,有几个合理的选择方案。 如果常量与现有的类或接口紧密相关,则应将其添加到该类或接口中。 例如,所有数字基本类型的包装类,如 Integer 和 Double,都会导出 MIN_VALUE 和 MAX_VALUE 常量。 如果常量最好被看作枚举类型的成员,则应该使用枚举类型(详见第 34 条)导出它们。 否则,你应该用一个不可实例化的工具类来导出常量(详见第 4 条)。 下是前面所示的 PhysicalConstants 示例的工具类的版本: |
| 60 | + |
| 61 | +// Constant utility class |
| 62 | +package com.effectivejava.science; |
| 63 | + |
| 64 | +public class PhysicalConstants { |
| 65 | + private PhysicalConstants() { } // Prevents instantiation |
| 66 | + |
| 67 | + public static final double AVOGADROS_NUMBER = 6.022_140_857e23; |
| 68 | + public static final double BOLTZMANN_CONST = 1.380_648_52e-23; |
| 69 | + public static final double ELECTRON_MASS = 9.109_383_56e-31; |
| 70 | +} |
| 71 | + 顺便提一下,请注意在数字文字中使用下划线字符(_)。 从 Java 7 开始,合法的下划线对数字字面量的值没有影响,但是如果使用得当的话可以使它们更容易阅读。 无论是固定的浮点数,如果他们包含五个或更多的连续数字,考虑将下划线添加到数字字面量中。 对于底数为 10 的数字,无论是整型还是浮点型的,都应该用下划线将数字分成三个数字组,表示一千的正负幂。 |
| 72 | + |
| 73 | + 通常,实用工具类要求客户端使用类名来限定常量名,例如 PhysicalConstants.AVOGADROS_NUMBER。 如果大量使用实用工具类导出的常量,则通过使用静态导入来限定具有类名的常量: |
| 74 | + |
| 75 | +// Use of static import to avoid qualifying constants |
| 76 | +import static com.effectivejava.science.PhysicalConstants.*; |
| 77 | + |
| 78 | +public class Test { |
| 79 | + double atoms(double mols) { |
| 80 | + return AVOGADROS_NUMBER * mols; |
| 81 | + } |
| 82 | + ... |
| 83 | + // Many more uses of PhysicalConstants justify static import |
| 84 | +} |
| 85 | + 总之,接口只能用于定义类型。 它们不应该仅用于导出常量。 |
| 86 | + |
| 87 | + 23. 类层次结构优于标签类 |
| 88 | + 有时你可能会碰到一个类,它的实例有两个或更多的风格,并且包含一个标签字段(tag field),表示实例的风格。 例如,考虑这个类,它可以表示一个圆形或矩形: |
| 89 | + |
| 90 | +// Tagged class - vastly inferior to a class hierarchy! |
| 91 | +class Figure { |
| 92 | + enum Shape { RECTANGLE, CIRCLE }; |
| 93 | + |
| 94 | + // Tag field - the shape of this figure |
| 95 | + final Shape shape; |
| 96 | + |
| 97 | + // These fields are used only if shape is RECTANGLE |
| 98 | + double length; |
| 99 | + double width; |
| 100 | + |
| 101 | + // This field is used only if shape is CIRCLE |
| 102 | + double radius; |
| 103 | + |
| 104 | + // Constructor for circle |
| 105 | + Figure(double radius) { |
| 106 | + shape = Shape.CIRCLE; |
| 107 | + this.radius = radius; |
| 108 | + } |
| 109 | + |
| 110 | + // Constructor for rectangle |
| 111 | + Figure(double length, double width) { |
| 112 | + shape = Shape.RECTANGLE; |
| 113 | + this.length = length; |
| 114 | + this.width = width; |
| 115 | + } |
| 116 | + |
| 117 | + double area() { |
| 118 | + switch(shape) { |
| 119 | + case RECTANGLE: |
| 120 | + return length * width; |
| 121 | + case CIRCLE: |
| 122 | + return Math.PI * (radius * radius); |
| 123 | + default: |
| 124 | + throw new AssertionError(shape); |
| 125 | + } |
| 126 | + } |
| 127 | +} |
| 128 | + 这样的标签类具有许多缺点。 它们充斥着杂乱无章的样板代码,包括枚举声明,标签字段和 switch 语句。 可读性更差,因为多个实现在一个类中混杂在一起。 内存使用增加,因为实例负担属于其他风格不相关的领域。 字段不能成为 final,除非构造方法初始化不相关的字段,导致更多的样板代码。 构造方法在编译器的帮助下,必须设置标签字段并初始化正确的数据字段:如果初始化错误的字段,程序将在运行时失败。 除非可以修改其源文件,否则不能将其添加到标记的类中。 如果你添加一个风格,你必须记得给每个 switch 语句添加一个 case,否则这个类将在运行时失败。 最后,一个实例的数据类型没有提供任何关于风格的线索。 总之,标签类是冗长的,容易出错的,而且效率低下。 |
| 129 | + |
| 130 | + 幸运的是,像 Java 这样的面向对象的语言为定义一个能够表示多种风格对象的单一数据类型提供了更好的选择:子类型化(subtyping)。标签类仅仅是一个类层次的简单的模仿。 |
| 131 | + |
| 132 | + 要将标签类转换为类层次,首先定义一个包含抽象方法的抽象类,该标签类的行为取决于标签值。 在 Figure 类中,只有一个这样的方法,就是 area 方法。 这个抽象类是类层次的根。 如果有任何方法的行为不依赖于标签的值,把它们放在这个类中。 同样,如果有所有的方法使用的数据字段,把它们放在这个类。Figure 类中不存在这种与类型无关的方法或字段。 |
| 133 | + |
| 134 | + 接下来,为原始标签类的每种类型定义一个根类的具体子类。 在我们的例子中,有两个类型:圆形和矩形。 在每个子类中包含特定于改类型的数据字段。 在我们的例子中,半径字段是属于圆的,长度和宽度字段都是矩形的。 还要在每个子类中包含根类中每个抽象方法的适当实现。 这里是对应于 Figure 类的类层次: |
| 135 | + |
| 136 | +// Class hierarchy replacement for a tagged class |
| 137 | +abstract class Figure { |
| 138 | + abstract double area(); |
| 139 | +} |
| 140 | + |
| 141 | +class Circle extends Figure { |
| 142 | + final double radius; |
| 143 | + |
| 144 | + Circle(double radius) { this.radius = radius; } |
| 145 | + |
| 146 | + @Override double area() { return Math.PI * (radius * radius); } |
| 147 | +} |
| 148 | +class Rectangle extends Figure { |
| 149 | + final double length; |
| 150 | + final double width; |
| 151 | + |
| 152 | + Rectangle(double length, double width) { |
| 153 | + this.length = length; |
| 154 | + this.width = width; |
| 155 | + } |
| 156 | + @Override double area() { return length * width; } |
| 157 | +} |
| 158 | + 这个类层次纠正了之前提到的标签类的每个缺点。 代码简单明了,不包含原文中的样板文件。 每种类型的实现都是由自己的类来分配的,而这些类都没有被无关的数据字段所占用。 所有的字段是 final 的。 编译器确保每个类的构造方法初始化其数据字段,并且每个类都有一个针对在根类中声明的每个抽象方法的实现。 这消除了由于缺少 switch-case 语句而导致的运行时失败的可能性。 多个程序员可以独立地继承类层次,并且可以相互操作,而无需访问根类的源代码。 每种类型都有一个独立的数据类型与之相关联,允许程序员指出变量的类型,并将变量和输入参数限制为特定的类型。 |
| 159 | + |
| 160 | + 类层次的另一个优点是可以使它们反映类型之间的自然层次关系,从而提高了灵活性,并提高了编译时类型检查的效率。 假设原始示例中的标签类也允许使用正方形。 类层次可以用来反映一个正方形是一种特殊的矩形(假设它们是不可变的): |
| 161 | + |
| 162 | +class Square extends Rectangle { |
| 163 | + Square(double side) { |
| 164 | + super(side, side); |
| 165 | + } |
| 166 | +} |
| 167 | + 请注意,上述层次结构中的字段是直接访问的,而不是通过访问器方法访问的。 这里是为了简洁起见,如果类层次是公开的(详见第 16 条),这将是一个糟糕的设计。 |
| 168 | + |
| 169 | + 总之,标签类很少有适用的情况。 如果你想写一个带有显式标签字段的类,请考虑标签字段是否可以被删除,并是否能被类层次结构替换。 当遇到一个带有标签字段的现有类时,可以考虑将其重构为一个类层次结构。 |
0 commit comments