Summer Blog

Effective Java

Chapter 3. Methods Common to All Objects

10.重写equals

一般不需要重写,在这种情况下类的每个实例仅仅与自身相等。若一定要比较两个实例的重写equals需要遵循以下规定:

  1. 自反性:对于任何非空引用 x,x.equals(x) 必须返回 true
  2. 对称性:对于任何非空引用 x 和 y,如果且仅当 y.equals(x) 返回 true 时 x.equals(y) 必须返回 true
  3. 传递性:对于任何非空引用 x、y、z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,则 x.equals(z) 必须返回 true
  4. 一致性:对于任何非空引用 x 和 y,如果在 equals 比较中使用的信息没有修改,则 x.equals(y) 的多次调用必须始终返回 true 或始终返回 false
  5. null比较一定是false

11.重写equals后一定要重新hashCode

根据 Object 规范,hashCode需遵循以下时具体约定:

  1. 如果没有修改 equals 方法中用以比较的信息,在应用程序的一次执行过程中对一个对象重复调用 hashCode 方法时,它必须始终返回相同的值。在应用程序的多次执行过程中,每个执行过程在该对象上获取的结果值可以不相同。
  2. 如果两个对象根据 equals(Object) 方法比较是相等的,那么在两个对象上调用 hashCode 就必须产生的结果是相同的整数。
  3. 如果两个对象根据 equals(Object) 方法比较并不相等,则不要求在每个对象上调用 hashCode 都必须产生不同的结果。 但是,程序员应该意识到,为不相等的对象生成不同的结果可能会提高散列表(hash tables)的性能。

当无法重写 hashCode 时,所违反第二个关键条款是:相等的对象必须具有相等的哈希码( hash codes)。

12.始终重写toString

toString 通用约定

建议所有的子类重写这个方法

14.考虑实现Comparable接口’

通过实现 Comparable 接口,可以让你的类与所有依赖此接口的通用算法和集合实现进行互操作。 只需少量的努力就可以获得巨大的能量。 几乎 Java 平台类库中的所有值类以及所有枚举类型(详见第 34 条)都实现了 Comparable 接口。 如果你正在编写具有明显自然顺序(如字母顺序,数字顺序或时间顺序)的值类,则应该实现 Comparable 接口。

Chapter 4. Classes and Interfaces

15.使类和成员的可访问性最小化

将设计良好的组件与设计不佳的组件区分开来的最重要的因素是,隐藏内部数据和其他实现细节的程度。一个设计良好的组件隐藏了它的所有实现细节,干净地将它的 API 与它的实现分离开来。然后,组件只通过它们的 API 进行通信,并且对彼此的内部工作一无所知。信息隐藏有利于模块化,使系统的每个部分的耦合性降低,方便单独开发、测试、修改、理解、使用;也降低了构建大型系统的风险,因为即使系统不能运行,各个独立的组件也可能是可用的。

对于成员(字段、方法、嵌套类和嵌套接口),有四种可能的访问级别,在这里,按照可访问性从小到大列出:

在 Java 9 中,作为模块系统(module system)的一部分引入了两个额外的隐式访问级别。模块包含一组包,就像一个包包含一组类一样。模块可以通过模块声明中的导出(export)声明显式地导出某些包 (这是 module-info.java 的源文件中包含的约定)。模块中的未导出包的公共和受保护成员在模块之外是不可访问的;在模块中,可访问性不受导出(export)声明的影响。使用模块系统允许你在模块之间共享类,而不让它们对整个系统可见。在未导出的包中,公共和受保护的公共类的成员会产生两个隐式访问级别,这是普通公共和受保护级别的内部类似的情况。这种共享的需求是相对少见的,并且可以通过重新安排包中的类来消除。

16.在公共类中使用访问方法而不是公共属性

// Degenerate classes like this should not be public!
class Point {
    public double x;
    public double y;
}

公共类不应该暴露可变属性。公共类暴露不可变属性的危害虽然仍然存在问题,但其危害较小。然而,有时需要包级私有或私有内部类来暴露属性,无论此类是否是可变的。

17.最小化可变性

不可变类简单来说是其实例不能被修改的类。包含在每个实例中的所有信息在对象的生命周期中是固定的,因此不会观察到任何变化。Java 平台类库包含许多不可变的类,包括 String 类、基本类型包装类以及 BigInteger 类和 BigDecimal 类。有很多很好的理由:不可变类比可变类更易于设计,实现和使用。他们不容易出错,并且更安全。

要使一个类成为不可变类,请遵循以下五条规则:

  1. 不要提供修改对象状态的方法
  2. 确保这个类不能被继承
  3. 把所有字段设置为 final
  4. 把所有的字段设置为 private
  5. 确保对任何可变组件的互斥访问

好处:

缺点:

18.组合优于继承

只有在子类真的是父类的子类型的情况下,继承才是合适的。换句话说,只有在两个类之间存在「is-a」关系的情况下,B 类才能继承 A 类。如果你试图让 B 类继承 A 类时,问自己这个问题:每个 B 都是 A 吗?如果你不能明确的以“是的”来回答这个问题,那么 B 就不应该继承 A。如果答案是否定的,那么 B 通常包含一个 A 的私有实例,并且暴露一个不同的 API:A 不是 B 的重要部分,只是其实现细节。

总之,继承是强大的,但它是有问题的,因为它违反封装。只有在子类和父类之间存在真正的子类型关系时才适用。即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。为了避免这种脆弱性,使用组合和转发代替继承,特别是如果存在一个合适的接口来实现包装类。包装类不仅比子类更健壮,而且更强大。

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

在被设计用来继承的类中,必须保证:

  1. 类必须以精心挑选的 protected 方法的形式,提供适当的钩子(hook),以便进入其内部工作中
  2. 测试为继承而设计的类的唯一方法是编写子类。在发布它之前,你必须通过编写子类来测试你的类
  3. 构造方法绝不能直接或间接调用可重写的方法

20.接口优于抽象类

所有类都可以实现接口来改进自己的功能,但必须是抽象类的子类,才能得到抽象类提供的能力。

可以通过提供一个抽象的骨架实现类(abstract skeletal implementation class)来与接口一起使用,将接口和抽象类的优点结合起来。 接口定义了类型,可能提供了一些默认的方法,而骨架实现类在原始接口方法的顶层实现了剩余的非原始接口方法。 继承骨架实现需要大部分的工作来实现一个接口。 这就是模板方法设计模式[Gamma95]。

21.为后代设计接口

应该避免使用默认方法向现有接口添加方法,应该谨慎考虑。

22.接口仅用来定义类型

当类实现接口时,该接口作为一种类型(type),可以用来引用类的实例。因此,一个类实现了一个接口,表明客户端可以如何处理类的实例。为其他目的定义接口是不合适的。

常量接口是对接口的糟糕使用。有几种替代方式:

  1. 将常量定义在其相关的接口中
  2. enum定义常量

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

标签类是一些类有明显的分类,但只通过其中的字段标记具体的分类,这样的设计增加了类的复杂性,降低了拓展性。可以通过创建一个基类和多个子类来代替标签类。

24.支持使用静态成员类而不是非静态类

嵌套类有四种:

  1. 静态成员类:最好把它看作一个普通类,恰好在另一类中声明。
  2. 非静态成员类:每一个实例都隐含地与其中包含的类的宿主实例相关联。它们的关系实在创建成员类实例时建立的,并且之后不能修改。每个实例都会隐藏一个外部引用给它的宿主实例,这个引用需要占用空间和时间,且可能导致宿主类在满足垃圾回收的条件时人然驻留内存中。例,ArrayList.Itr
  3. 匿名类:在使用时才被声明和实例化,不是宿主类的成员。不能声明一个匿名类继承多个类,或实现多个接口。
  4. 局部类

25.将源文件限制为单个顶级类

虽然Java编译器允许在单个源文件中定义多个顶级类,但这样做没有任何好处,并且存在重大风险。风险源于在源文件中定义多个顶级类使得为类提供多个定义成为可能。使用哪个定义会受到源文件传递给编译器的顺序的影响。


comments powered by Disqus