有时你可能会碰到一个类,它的实例有两个或更多的风格,并且包含一个标签字段(tag field),表示实例的风格。 例如,考虑这个类,它可以表示一个圆形或矩形:
// Tagged class - vastly inferior to a class hierarchy!
class Figure {
enum Shape ;
// Tag field - the shape of this figure
final Shape shape;
// These fields are used only if shape is RECTANGLE
double length;
double width;
// This field is used only if shape is CIRCLE
double radius;
// Constructor for circle
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// Constructor for rectangle
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch(shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError(shape);
}
}
}
这样的标签类具有许多缺点。 它们充斥着杂乱无章的样板代码,包括枚举声明,标签字段和 switch
语句。 可读性更差,因为多个实现在一个类中混杂在一起。 内存使用增加,因为实例负担属于其他风格不相关的领域。 字段不能成为 final,除非构造方法初始化不相关的字段,导致更多的样板代码。 构造方法在编译器的帮助下,必须设置标签字段并初始化正确的数据字段:如果初始化错误的字段,程序将在运行时失败。 除非可以修改其源文件,否则不能将其添加到标记的类中。 如果你添加一个风格,你必须记得给每个 switch
语句添加一个 case
,否则这个类将在运行时失败。 最后,一个实例的数据类型没有提供任何关于风格的线索。 总之,标签类是冗长的,容易出错的,而且效率低下。
幸运的是,像 Java 这样的面向对象的语言为定义一个能够表示多种风格对象的单一数据类型提供了更好的选择:子类型化(subtyping)。标签类仅仅是一个类层次的简单的模仿。
要将标签类转换为类层次,首先定义一个包含抽象方法的抽象类,该标签类的行为取决于标签值。 在 Figure
类中,只有一个这样的方法,就是 area
方法。 这个抽象类是类层次的根。 如果有任何方法的行为不依赖于标签的值,把它们放在这个类中。 同样,如果有所有的方法使用的数据字段,把它们放在这个类。Figure
类中不存在这种与类型无关的方法或字段。
接下来,为原始标签类的每种类型定义一个根类的具体子类。 在我们的例子中,有两个类型:圆形和矩形。 在每个子类中包含特定于改类型的数据字段。 在我们的例子中,半径字段是属于圆的,长度和宽度字段都是矩形的。 还要在每个子类中包含根类中每个抽象方法的适当实现。 这里是对应于 Figure
类的类层次:
// Class hierarchy replacement for a tagged class
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
Circle(double radius)
@Override double area() { return Math.PI * (radius * radius); }
}
class Rectangle extends Figure {
final double length;
final double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override double area()
}
这个类层次纠正了之前提到的标签类的每个缺点。 代码简单明了,不包含原文中的样板文件。 每种类型的实现都是由自己的类来分配的,而这些类都没有被无关的数据字段所占用。 所有的字段是 final
的。 编译器确保每个类的构造方法初始化其数据字段,并且每个类都有一个针对在根类中声明的每个抽象方法的实现。 这消除了由于缺少 switch-case
语句而导致的运行时失败的可能性。 多个程序员可以独立地继承类层次,并且可以相互操作,而无需访问根类的源代码。 每种类型都有一个独立的数据类型与之相关联,允许程序员指出变量的类型,并将变量和输入参数限制为特定的类型。
类层次的另一个优点是可以使它们反映类型之间的自然层次关系,从而提高了灵活性,并提高了编译时类型检查的效率。 假设原始示例中的标签类也允许使用正方形。 类层次可以用来反映一个正方形是一种特殊的矩形(假设它们是不可变的):
class Square extends Rectangle {
Square(double side) {
super(side, side);
}
}
请注意,上述层次结构中的字段是直接访问的,而不是通过访问器方法访问的。 这里是为了简洁起见,如果类层次是公开的(详见第 16 条),这将是一个糟糕的设计。
总之,标签类很少有适用的情况。 如果你想写一个带有显式标签字段的类,请考虑标签字段是否可以被删除,并是否能被类层次结构替换。 当遇到一个带有标签字段的现有类时,可以考虑将其重构为一个类层次结构。
文章列表
- 高效Java编程-01. 考虑使用静态工厂方法替代构造方法
- 高效Java编程-02. 当构造方法参数过多时使用builder模式
- 高效Java编程-03. 使用私有构造方法或枚类实现Singleton属性
- 高效Java编程-04. 使用私有构造方法执行非实例化
- 高效Java编程-05. 依赖注入优于硬连接资源(hardwiring resources)
- 高效Java编程-06. 避免创建不必要的对象
- 高效Java编程-07. 消除过期的对象引用
- 高效Java编程-08. 避免使用Finalizer和Cleaner机制
- 高效Java编程-09. 使用try-with-resources语句替代try-finally语句
- 高效Java编程-10. 重写equals方法时遵守通用约定
- 高效Java编程-11. 重写equals方法时同时也要重写hashcode方法
- 高效Java编程-12. 始终重写 toString 方法
- 高效Java编程-13. 谨慎地重写 clone 方法
- 高效Java编程-14. 考虑实现Comparable接口
- 高效Java编程-15. 使类和成员的可访问性最小化
- 高效Java编程-16. 在公共类中使用访问方法而不是公共属性
- 高效Java编程-17. 最小化可变性
- 高效Java编程-18. 组合优于继承
- 高效Java编程-19. 要么设计继承并提供文档说明,要么禁用继承
- 高效Java编程-20. 接口优于抽象类
- 高效Java编程-21. 为后代设计接口
- 高效Java编程-22. 接口仅用来定义类型
- 高效Java编程-23. 类层次结构优于标签类
- 高效Java编程-24. 支持使用静态成员类而不是非静态类
- 高效Java编程-25. 将源文件限制为单个顶级类
- 高效Java编程-26. 不要使用原始类型
- 高效Java编程-27. 消除非检查警告
- 高效Java编程-28. 列表优于数组
- 高效Java编程-29. 优先考虑泛型
- 高效Java编程-30. 优先使用泛型方法
- 高效Java编程-31. 使用限定通配符来增加API的灵活性
- 高效Java编程-32. 合理地结合泛型和可变参数
- 高效Java编程-33. 优先考虑类型安全的异构容器
- 高效Java编程-34. 使用枚举类型替代整型常量
- 高效Java编程-35. 使用实例属性替代序数
- 高效Java编程-汇总