第 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 的例子:
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
最后的告诫:
- 覆盖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 值。
例子:
// Typical hashCode method @Override public int hashCode() { int result = Short.hashCode(areaCode); result = 31 * result + Short.hashCode(prefix); result = 31 * result + Short.hashCode(lineNum); return result; }
写完之后验证,问自己“相等的实例是否都具有相等的散列码”。
总之,每当覆盖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实现可能是灾难性的。
例子:
ublic class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { this.elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // Eliminate obsolete reference return result; } // Ensure space for at least one more element. private void ensureCapacity() { if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } }
上述例子希望做成可克隆的,如果clone方法仅返回super.clone()调用的对象,那么生成的Stack实例在其size 属性中具有正确的值,
但elements属性引用与原始Stack实例相同的数组。 修改原始实例将破坏克隆中的约束条件,反之亦然。
你会很快发现你的程序产生了无意义的结果,或者抛出NullPointerException异常。
实际上,clone方法就是另一个构造器,必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。
上述例子在elements数组中递归地调用clone:
// Clone method for class with references to mutable state @Override public Stack clone() { try { Stack result = (Stack) super.clone(); result.elements = elements.clone(); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } }
在数组上调用clone返回的数组,编译时的类型与被克隆数组的类型相同,这是复制数组的最佳习惯。
如果elements属性是final的,则以前的解决方案将不起作用,因为克隆将被禁止向该属性分配新的值。
这是一个基本的问题:像序列化一样,Cloneable体系结构与引用可变对象的final 属性的正常使用不兼容。
仅仅递归地调用clone方法并不总是足够的。
例如一个类包含一个散列桶数组,每个散列通都指向键-值对链表第一项,是一个单向链表。
如果仅克隆散列数组,但是这个数组引用的链表与原始对象一样,容易引起克隆对象和原始对象中不确定行为。所有必须单独地拷贝并组成每个桶的链表。
简而言之,实现Cloneable的所有类应该重写公共clone方法,而这个方法的返回类型是类本身。
这个方法应该首先调用super.clone,然后修复任何需要修复的属性。
通常,这意味着复制任何包含内部“深层结构”的可变对象,并用指向新对象的引用来代替原来指向这些对象的引用。
虽然这些内部拷贝通常可以通过递归调用clone来实现,但这并不总是最好的方法。
如果类只包含基本类型或对不可变对象的引用,那么很可能是没有属性需要修复的情况。
这个规则也有例外, 例如,表示序列号或其他唯一ID的属性即使是基本类型的或不可变的,也需要被修正。
对象拷贝的更好方法时提供一个拷贝构造器(copy constructor)或拷贝工厂(copy factory)。
// Copy constructor public Yum(Yum yum) { ... };
// Copy factory public static Yum newInstance(Yum yum) { ... };
总之,考虑到与Cloneable接口相关的所有问题,新的接口不应该继承它,新的可扩展类不应该实现它。
虽然实现Cloneable接口对于final类没有什么危害,但应该将其视为性能优化的角度,仅在极少数情况下才是合理的(条目67)。
通常,复制功能最好由构造方法或工厂提供。 这个规则的一个明显的例外是数组,它最好用 clone方法复制。
第 14 条:考虑实现Comparable接口
与本章讨论的其他方法不同,compareTo 方法并没有在 Object 类中声明。
相反,它是 Comparable 接口中的唯一方法。 通过实现 Comparable 接口,一个类表明它的实例有一个自然序( natural ordering )。
通过实现 Comparable 接口,可以让你的类与所有依赖此接口的泛型算法和集合实现进行交互操作。
Java 平台类库中几乎所有值类以及所有枚举类型(条款 34)都实现了 Comparable 接口。
如果你正在编写具有明显自然序(如字母顺序、数字顺序或时间顺序)的值类,则应该实现 Comparable 接口:
public interface Comparable<T> { int compareTo(T t); }
将此对象与指定的对象按照排序进行比较。返回值可能为负整数,零或正整数,对应此对象小于,等于或大于指定的对象。
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 的静态导入,以便可以通过其简单名称来引用比较器静态方法。
// Comparable with comparator construction methods private static final Comparator<PhoneNumber> COMPARATOR = comparingInt((PhoneNumber pn) -> pn.areaCode) .thenComparingInt(pn -> pn.prefix) .thenComparingInt(pn -> pn.lineNum); public int compareTo(PhoneNumber pn) { return COMPARATOR.compare(this, pn); }