第 10 条:覆盖equals时请遵守通用约定

在不覆盖equals方法下,类的每个实例都只与它自身相等。

  • 类的每个实例本质上都是唯一的。
  • 类不需要提供一个”逻辑相等(logical equality)”的测试功能。
  • 父类已经重写了 equals 方法,并且父类的行为完全适合于该子类。
  • 类是私有的或包级私有的,并且可以确定它的 equals 方法永远不会被调用。

什么时候需要覆盖equals方法?
  如果一个类包含一个逻辑相等( logical equality)的概念——此概念有别于对象同一性(object identity),而且父类还没有重写过 equals 方法。

这通常用在值类( value classes)的情况。

在覆盖equals 方法时,必须遵守它的通用规范。下面是 Object 类注释里的规范:

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

编写高质量 equals 方法的秘诀:

  • 使用 == 运算符检查参数是否为该对象的引用。如果是,返回true。
  • 使用 instanceof 运算符来检查参数是否具有正确的类型。 如果不是,则返回 false。
  • 将参数转换为正确的类型。
  • 对于类中的每个关键域(属性),检查参数的属性是否与该对象对应的属性相匹配。
  • 对于类型为非 float 或 double 的基本类型,使用 == 运算符进行比较;对于对象引用属性,递归地调用 equals 方法;对于 float 基本类型的属性,使用静态方法 Float.compare(float, float);对于 double 基本类型的属性,使用 Double.compare(double, double) 方法。
  • equals 方法的性能可能受到属性比较顺序的影响。为了获得最佳性能,你应该首先比较最可能不同的属性和开销比较小的属性。

例如String 的例子:

  1. public boolean equals(Object anObject) {
  2. if (this == anObject) {
  3. return true;
  4. }
  5. if (anObject instanceof String) {
  6. String anotherString = (String)anObject;
  7. int n = value.length;
  8. if (n == anotherString.value.length) {
  9. char v1[] = value;
  10. char v2[] = anotherString.value;
  11. int i = 0;
  12. while (n-- != 0) {
  13. if (v1[i] != v2[i])
  14. return false;
  15. i++;
  16. }
  17. return true;
  18. }
  19. }
  20. return false;
  21. }

最后的告诫:

  • 覆盖equals时总要覆盖hashCode。
  • 不要企图让equals方法过于智能。
  • 不要将equals声明中的Object对象替换为其他的类型。

  总之,不要轻易覆盖equals方法,除非迫不得已。因为很多情况下,从Object继承的实现正是你想要的。

  如果覆盖equals方法,一定要比较这个类的所有关键域,并且确保遵守equals合约的五个条款。

第 11 条:覆盖equals时总要覆盖hashCode

在每一个重写 equals 方法的类中,都要重写 hashCode 方法。
  如果不这样做,你的类会违反 hashCode 的通用约定,这会阻止它在 HashMap 和 HashSet 这样的集合中正常工作。

Object源码约定内容:

  • 在一个应用程序执行过程中,如果在 equals 方法比较中没有修改任何信息,在一个对象上重复调用 hashCode 方法必须始终返回相同的值。从一个应用程序到另一个应用程序时返回的值可以是不一致的。
  • 如果两个对象根据 equals(Object) 方法比较是相等的,那么在这两个对象上调用 hashCode 就必须产生相同的整数结果。
  • 如果两个对象根据 equals(Object) 方法比较并不相等,不要求在每个对象上调用 hashCode 都必须产生不同的结果。 为不相等的对象生成不同的结果可能会提高散列表(hash tables)的性能。

没有覆盖hashCode违反上述规约第二条:相等对象必须具有相等的散列码(hashCode)。

一个好的 hash 方法趋向于为不相等的实例生成不相等的哈希码。

理想情况下,hash 方法为集合中不相等的实例均匀地分配 int 范围内的哈希码。实现这种理想情况可能很困难。

简单步骤:
  1. 声明一个 int 类型的变量 result,并将其初始化为对象中第一个重要属性 c 的哈希码,如下面步骤 2.a 中所计算的那样。
  2. 对于对象中剩余的重要属性 f ,执行以下操作:
    a. 为属性 f 与计算一个 int 类型的哈希码 c:
      i. 如果这个属性是基本类型,使用 Type.hashCode(f) 方法计算,其中 Type 类是对应属性 f 的包装类。
      ii. 如果该属性是一个对象引用,并且该类的 equals 方法通过递归调用 equals 来比较该属性,那么递归地调用 hashCode 方法。

如果需要更复杂的比较,则计算此字段的“范式”(canonical representation),并在范式上调用 hashCode 。

如果该字段的值为空,则使用 0(也可以使用其他常数,但通常使用 0 表示)。
      iii. 如果属性 f 是一个数组,把数组中每个重要的元素都看作是一个独立的属性。

如果数组没有重要的元素,则使用一个常量,最好不要为0。如果所有元素都很重要,则使用 Arrays.hashCode 方法。
    b. 将步骤 2.a 中计算出的哈希码 c 合并为如下结果:result = 31 * result + c;
  3. 返回 result 值。

例子:

  1. // Typical hashCode method
  2. @Override public int hashCode() {
  3. int result = Short.hashCode(areaCode);
  4. result = 31 * result + Short.hashCode(prefix);
  5. result = 31 * result + Short.hashCode(lineNum);
  6. return result;
  7. }

写完之后验证,问自己“相等的实例是否都具有相等的散列码”。

总之,每当覆盖equals方法时都必须覆盖hashCode,否则程序将无法正确运行。
  还可以利用AutoValue(google)生成equals和hashCode方法,不必手工编写,可以省略测试。部分IDE也提供类似的部分功能。

第 12 条:始终要覆盖toString

提供好的易读的toString实现可以让使用这个类的系统更容易调试。
  在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息。无论是否指定格式,应该在文档中明确地表明你的意图。
  在静态工具类中编写toString方法时没有意义的,也不用在大多数枚举类型中编写toString方法。
  Google开源的AutoValue会替你生成toString方法。

总之,要在你编写的每一个可实例化的类中覆盖Object的toString实现,除非已经在超类中这么做了。

  这样会让类的使用易于调试。toString方法应该返回一个关于对象的简洁、有用的描述。

第 13 条:谨慎地覆盖clone

Cloneable接口的目的是作为对象的一个接口,表明这样的对象允许克隆(clone)。
  实现Cloneable接口的类是为了提供一个功能适当的公有的clone方法。

假设你希望在一个类中实现Cloneable接口,它的父类提供了一个行为良好的 clone方法。

首先调用super.clone。 得到的对象将是原始的完全功能的复制品。 在你的类中声明的任何属性将具有与原始属性相同的值。

如果每个属性包含原始值或对不可变对象的引用,则返回的对象可能正是你所需要的,在这种情况下,不需要进一步的处理。(浅拷贝)

不可变的类永远都不应该提供clone方法。
  如果对象包含引用可变对象的属性,则前面显示的简单clone实现可能是灾难性的。

例子:

  1. ublic class Stack {
  2.  
  3. private Object[] elements;
  4. private int size = 0;
  5. private static final int DEFAULT_INITIAL_CAPACITY = 16;
  6.  
  7. public Stack() {
  8. this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
  9. }
  10.  
  11. public void push(Object e) {
  12. ensureCapacity();
  13. elements[size++] = e;
  14. }
  15.  
  16. public Object pop() {
  17. if (size == 0)
  18. throw new EmptyStackException();
  19. Object result = elements[--size];
  20.  
  21. elements[size] = null; // Eliminate obsolete reference
  22. return result;
  23. }
  24.  
  25. // Ensure space for at least one more element.
  26. private void ensureCapacity() {
  27. if (elements.length == size)
  28. elements = Arrays.copyOf(elements, 2 * size + 1);
  29. }
  30. }

上述例子希望做成可克隆的,如果clone方法仅返回super.clone()调用的对象,那么生成的Stack实例在其size 属性中具有正确的值,

但elements属性引用与原始Stack实例相同的数组。 修改原始实例将破坏克隆中的约束条件,反之亦然。

你会很快发现你的程序产生了无意义的结果,或者抛出NullPointerException异常。

实际上,clone方法就是另一个构造器,必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。
 上述例子在elements数组中递归地调用clone:

  1. // Clone method for class with references to mutable state
  2. @Override public Stack clone() {
  3. try {
  4. Stack result = (Stack) super.clone();
  5. result.elements = elements.clone();
  6. return result;
  7. } catch (CloneNotSupportedException e) {
  8. throw new AssertionError();
  9. }
  10. }

在数组上调用clone返回的数组,编译时的类型与被克隆数组的类型相同,这是复制数组的最佳习惯。

如果elements属性是final的,则以前的解决方案将不起作用,因为克隆将被禁止向该属性分配新的值。

这是一个基本的问题:像序列化一样,Cloneable体系结构与引用可变对象的final 属性的正常使用不兼容。

仅仅递归地调用clone方法并不总是足够的。

例如一个类包含一个散列桶数组,每个散列通都指向键-值对链表第一项,是一个单向链表。

如果仅克隆散列数组,但是这个数组引用的链表与原始对象一样,容易引起克隆对象和原始对象中不确定行为。所有必须单独地拷贝并组成每个桶的链表。

简而言之,实现Cloneable的所有类应该重写公共clone方法,而这个方法的返回类型是类本身。

这个方法应该首先调用super.clone,然后修复任何需要修复的属性。

通常,这意味着复制任何包含内部“深层结构”的可变对象,并用指向新对象的引用来代替原来指向这些对象的引用。

虽然这些内部拷贝通常可以通过递归调用clone来实现,但这并不总是最好的方法。

如果类只包含基本类型或对不可变对象的引用,那么很可能是没有属性需要修复的情况。

这个规则也有例外, 例如,表示序列号或其他唯一ID的属性即使是基本类型的或不可变的,也需要被修正。

对象拷贝的更好方法时提供一个拷贝构造器(copy constructor)或拷贝工厂(copy factory)。

  1. // Copy constructor
  2. public Yum(Yum yum) { ... };
  1. // Copy factory
  2. public static Yum newInstance(Yum yum) { ... };

总之,考虑到与Cloneable接口相关的所有问题,新的接口不应该继承它,新的可扩展类不应该实现它。

虽然实现Cloneable接口对于final类没有什么危害,但应该将其视为性能优化的角度,仅在极少数情况下才是合理的(条目67)。

通常,复制功能最好由构造方法或工厂提供。 这个规则的一个明显的例外是数组,它最好用 clone方法复制。

第 14 条:考虑实现Comparable接口

与本章讨论的其他方法不同,compareTo 方法并没有在 Object 类中声明。

相反,它是 Comparable 接口中的唯一方法。 通过实现 Comparable 接口,一个类表明它的实例有一个自然序( natural ordering )。

  通过实现 Comparable 接口,可以让你的类与所有依赖此接口的泛型算法和集合实现进行交互操作。

  Java 平台类库中几乎所有值类以及所有枚举类型(条款 34)都实现了 Comparable 接口。

  如果你正在编写具有明显自然序(如字母顺序、数字顺序或时间顺序)的值类,则应该实现 Comparable 接口:

  1. public interface Comparable<T> {
  2. int compareTo(T t);
  3. }

将此对象与指定的对象按照排序进行比较。返回值可能为负整数,零或正整数,对应此对象小于,等于或大于指定的对象。
  compareTo不能跨越不同类型的对象进行比较,在比较不同类型的对象时,抛出ClassCastException异常。

考虑 BigDecimal 类,其 compareTo 方法与 equals 不一致。

如果你创建一个空的 HashSet 实例,然后添加 new BigDecimal("1.0") 和 new BigDecimal("1.00"),则该集合将包含两个元素,

因为用 equals 方法进行比较时,添加到集合的两个 BigDecimal 实例是不相等的。

但是,如果使用 TreeSet 而不是 HashSet 执行相同的过程,则该集合将只包含一个元素,因为使用 compareTo 方法进行比较时,两个 BigDecimal 实例是相等的。

在 Java 7 中,静态比较方法被添加到 Java 的所有包装类中。在 compareTo 方法中使用关系运算符 < 和 > 是冗长且容易出错的,不再推荐。

在 Java 8 中 Comparator 接口提供了一系列比较器方法,可以流畅地构建比较器。

许多程序员更喜欢这种方法的简洁性,尽管它会牺牲一定地性能。在使用这种方法时,考虑使用 Java 的静态导入,以便可以通过其简单名称来引用比较器静态方法。

  1. // Comparable with comparator construction methods
  2. private static final Comparator<PhoneNumber> COMPARATOR =
  3. comparingInt((PhoneNumber pn) -> pn.areaCode)
  4. .thenComparingInt(pn -> pn.prefix)
  5. .thenComparingInt(pn -> pn.lineNum);
  6. public int compareTo(PhoneNumber pn) {
  7. return COMPARATOR.compare(this, pn);
  8. }

[Java读书笔记] Effective Java(Third Edition) 第 3 章 对于所有对象都通用的方法的更多相关文章

  1. [Effective Java]第三章 对所有对象都通用的方法

    声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...

  2. [Effective Java 读书笔记] 第三章 对所有对象都通用的方法 第八 ---- 九条

    这一章主要讲解Object类中的方法, Object类是所有类的父类,所以它的方法也称得上是所有对象都通用的方法 第八条 覆盖equals时需要遵守的约定 Object中的equals实现,就是直接对 ...

  3. [Java读书笔记] Effective Java(Third Edition) 第 7 章 Lambda和Stream

    在Java 8中,添加了函数式接口(functional interface),Lambda表达式和方法引用(method reference),使得创建函数对象(function object)变得 ...

  4. [Java读书笔记] Effective Java(Third Edition) 第 6 章 枚举和注解

    Java支持两种引用类型的特殊用途的系列:一种称为枚举类型(enum type)的类和一种称为注解类型(annotation type)的接口. 第34条:用enum代替int常量 枚举是其合法值由一 ...

  5. [Java读书笔记] Effective Java(Third Edition) 第 4 章 类和接口

    第 15 条: 使类和成员的可访问性最小化 软件设计基本原则:信息隐藏和封装. 信息隐藏可以有效解耦,使组件可以独立地开发.测试.优化.使用和修改.   经验法则:尽可能地使每个类或者成员不被外界访问 ...

  6. [Java读书笔记] Effective Java(Third Edition) 第2章 创建和销毁对象

      第 1 条:用静态工厂方法代替构造器 对于类而言,获取一个实例的方法,传统是提供一个共有的构造器. 类可以提供一个公有静态工厂方法(static factory method), 它只是一个返回类 ...

  7. Effective Java读书笔记——第三章 对于全部对象都通用的方法

    第8条:覆盖equals时请遵守通用的约定 设计Object类的目的就是用来覆盖的,它全部的非final方法都是用来被覆盖的(equals.hashcode.clone.finalize)都有通用约定 ...

  8. 《Effective Java》第3章 对于所有对象都通用的方法

    第8条:覆盖equals时请遵守通用约定 覆盖equals方法看起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重.最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每 ...

  9. 《Effective Java》第2章 对所有对象都通用的方法

    第10条:覆盖equals时,请遵守通用约定 1.使用==来比较两个对象的时候,比较的是两个对象在内存中的地址是否相同(两个引用指向的是否为同一个对象):Object中定义的equals方法也是这样比 ...

随机推荐

  1. maskrcnn-benchmark训练自己数据

    需要修改的地方 1. ./maskrcnn_benchmark/data/datasets/voc.py 将CLASSES 内容改为自己的数据标签 2. ./maskrcnn_benchmark/co ...

  2. Dart 面向对象 类 方法

    Dart是一门使用类和单继承的面向对象语言,所有的对象都是类的实例,并且所有的类都是Object的子类. 面向对象编程(OOP)的三个基本特征是:封装.继承.多态 封装:封装是对象和类概念的主要特性. ...

  3. redis——redis的一些核心把握

    redis单线程,为什么比较快 单线程指的是网络请求模块使用了一个线程(所以不需考虑并发安全性),即一个线程处理所有网络请求,其他模块仍用了多个线程.redis能够快速执行的原因有三点: (1) 绝大 ...

  4. String 类的常用方法都有那些?(未完成)

    String 类的常用方法都有那些?(未完成)

  5. zencart价格筛选插件

    1.首先,新建文件includes\modules\sideboxes\price_range.php <?php function zen_count_products_in_price($p ...

  6. SQL 归纳

    查询父节点的所有子节点: SELECT * FROM menu m START WITH m.ID_ = '402882836068695f0160688eebf70006' CONNECT BY m ...

  7. grep匹配命令

    关于匹配的实例: 统计所有包含“48”字符的行有多少行 grep -c "48" demo.txt   不区分大小写查找“May”所有的行) grep -i "May&q ...

  8. ext系统的超级块

    什么是超级块 如果说inode块是Linux操作系统中文件的核心,那么超级块就是文件系统的心脏.启动Lnux操作系统后,发现某个文件系统无法使用,很有 可能就是超级块出现了问题.为什么这个超级块有这么 ...

  9. hlslcc

    https://cdn2.unrealengine.com/Resources/files/UE4_OpenGL4_GDC2014-514746542.pdf ue的跨平台编译器 hlsl cross ...

  10. 简单的理解 StringBuffer/StringBuilder/String 的区别

    StringBuffer/StringBuilder/String 的区别 这个三类之间主要的区别:运行速度,线程安全两个方面. 速度方面(快到慢): StringBuilder > Strin ...