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

类层次结构优于标签类

# 23. 类层次结构优于标签类

有时你可能会碰到一个类,它的实例有两个或更多的风格,并且包含一个标签字段(tag field),表示实例的风格。 例如,考虑这个类,它可以表示一个圆形或矩形:

// Tagged class - vastly inferior to a class hierarchy!
class Figure {
    enum Shape { RECTANGLE, CIRCLE };

    // 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);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

这样的标签类具有许多缺点。 它们充斥着杂乱无章的样板代码,包括枚举声明,标签字段和 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) { this.radius = 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() { return length * width; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这个类层次纠正了之前提到的标签类的每个缺点。 代码简单明了,不包含原文中的样板文件。 每种类型的实现都是由自己的类来分配的,而这些类都没有被无关的数据字段所占用。 所有的字段是 final 的。 编译器确保每个类的构造方法初始化其数据字段,并且每个类都有一个针对在根类中声明的每个抽象方法的实现。 这消除了由于缺少 switch-case 语句而导致的运行时失败的可能性。 多个程序员可以独立地继承类层次,并且可以相互操作,而无需访问根类的源代码。 每种类型都有一个独立的数据类型与之相关联,允许程序员指出变量的类型,并将变量和输入参数限制为特定的类型。

类层次的另一个优点是可以使它们反映类型之间的自然层次关系,从而提高了灵活性,并提高了编译时类型检查的效率。 假设原始示例中的标签类也允许使用正方形。 类层次可以用来反映一个正方形是一种特殊的矩形(假设它们是不可变的):

class Square extends Rectangle {
    Square(double side) {
        super(side, side);
    }
}
1
2
3
4
5

请注意,上述层次结构中的字段是直接访问的,而不是通过访问器方法访问的。 这里是为了简洁起见,如果类层次是公开的(详见第 16 条),这将是一个糟糕的设计。

总之,标签类很少有适用的情况。 如果你想写一个带有显式标签字段的类,请考虑标签字段是否可以被删除,并是否能被类层次结构替换。 当遇到一个带有标签字段的现有类时,可以考虑将其重构为一个类层次结构。

Last Updated: 2023/01/30, 11:01:00
接口仅用来定义类型
支持使用静态成员类而不是非静态类

← 接口仅用来定义类型 支持使用静态成员类而不是非静态类→

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