由前文知java提供了抽象数据类型ADT,使数据类型不再表现于具体属性,而表现于ADT在用户眼中的特征。可以想到 ,定义并阐明ADT的相等关系是很有必要的。
一个等价关系E具有如下性质
定义不可变ADT的相等关系(以下讨论均为不可变类型)有两种方法
1、根据AF定义
AF为R空间到A空间的一个映射,a与b相等当且仅当AF(a)=AF(b)
2、根据observer定义
observer为用户观察ADT的手段,故定义当所有的观察器都区分不了a与b时,a与b相等
下面是一个例子
public class Duration {
private final int mins;
private final int secs;
// Rep invariant:
// mins >= 0, secs >= 0
// Abstraction function:
// AF(min, secs) = the span of time of mins minutes and secs seconds
/** Make a duration lasting for m minutes and s seconds. */
public Duration(int m, int s) {
mins = m; secs = s;
}
/** @return length of this duration in seconds */
public long getLength() {
return (long)mins*60 + secs;
}
}
Duration d1 = new Duration (1, 2);
Duration d2 = new Duration (1, 3);
Duration d3 = new Duration (0, 62);
Duration d4 = new Duration (1, 2);
站在AF的角度,d1d2d4是相等的,它们都映射到62秒
站在观察器的角度,d1d2d4是相等的,getLength()都得到62秒
值得一提的是两个角度得到的结果应始终保持一致
在java中,==判定两边是否指向一块内存,即判定引用相等性(reference equality);而equals()判断内容是否相等,即判定对象相等性(object equality)
在其他语言中也有不同
java中object类中的equals方法就是简单的引用比较
public class Object {
...
public boolean equals(Object that) {
return this == that;
}
}
==与object的equals()方法是无法改变的,所以我们设计ADT时有必要重写equals()方法来根据需求判定相等
对于上述的Duration类,我们很自然地如下实现
// Problematic definition of equals()
public boolean equals(Duration that) {
return this.getLength() == that.getLength();
}
但在某些情况会得出错误的结果
Duration d1 = new Duration (1, 2);
Duration d2 = new Duration (1, 2);
Object o2 = d2;
d1.equals(d2) → true
d1.equals(o2) → false
原因很简单,由于参数列表不同,在Duration里写的equals(Duration that)并没有重写Object类中的equals(Object that),于是Duration中实际上有两个equals方法。
Object类的规约十分重要,一般称为Object Contract,其中的equals方法强调了重写也应遵守的原则:
下面看看如果不遵守上述原则重写equals会发生什么错误
首先,我们想允许一个一定程度的误差范围
@Override
public boolean equals(Object that) {
return that instanceof Duration && this.sameValue((Duration)that);
}
private static final int CLOCK_SKEW = 5; // seconds
// returns true iff this and that represent the same abstract value within a clock-skew tolerance
private boolean sameValue(Duration that) {
return Math.abs(this.getLength() - that.getLength()) <= CLOCK_SKEW;
}
其中instanceof检测that是否在运行时为Duration类型(应保证只在equals中使用此方法)
可以看到这样的equals不是等价关系(没有传递性),会导致如下问题
Duration d_1_00 = new Duration(1, 0);
Duration d_0_57 = new Duration(0, 57);
Duration d_1_03 = new Duration(1, 3);
d_0_57.equals(d_1_00) -> true
d_1_00.equals(d_1_03) -> true
d_0_57.equals(d_1_03) -> false
紧接前文,现在我们来看看如果equals相等的两个对象hashcode不同会发生什么
hash散列技术将一个个对象通过键值映射到一个个桶中,每个桶有不止一个对象(它们的hashcode相同)。所以,如果两个equals方法判断出相等的两个对象A,B,若它们的hashcode不同,它们会被分配到不同的桶中。这样在查找的时候(通过hashcode锁定桶)可能就会失败。
可以看到Object类本身的hashcode实现是遵循这个原则的,equals的Object意味着两个对象地址相同,故hashcode也相同
public class Object {
...
public boolean equals(Object that) { return this == that; }
public int hashCode() { return /* the memory address of this */; }
}
但如果我们的ADT不重写hashcode,是会发生错误的
Duration d1 = new Duration(1, 2);
Duration d2 = new Duration(1, 2);
d1.equals(d2) → true
d1.hashCode() → 2392
d2.hashCode() → 4823
对于上文提到的Duration类,稍加改变就能使其满足要求
@Override
public int hashCode() {
return (int) getLength();
所以可以明确地说:重写equals方法后一定要重写hashcode方法
同时为了避免拼写错误(如误拼为hashcode),建议带上@Override,这样在误拼的时候会提示没有重写任何方法。
可变类型的相等定于与不可变类型大同小异,一般从以下两个方面考虑:
对于不可变类型来说,观察等价性与表现等价性是完全相同的,因为不可变类型无法被改变,当在某一时刻被观测到两者相同时,这两者会一直保持相同。
值得注意的是java的list和array实现的equals并不相同,其他collection如map、set与list相同
// for arrays, java uses behavioral equality
int[] arrayA = new int[] { 1, 2, 3 };
int[] arrayB = arrayA;
int[] arrayC = new int[] { 1, 2, 3 };
arrayA.equals(arrayB) -> true
arrayA.equals(arrayC) -> false
arrayA == arrayB -> true
arrayA == arrayC -> false
// for list, java uses observational equality
List<Integer> listA = List.of(1, 2, 3);
List<Integer> listB = new ArrayList<>(listA);
List<Integer> listC = listB;
ListA.equals(ListC) -> true
ListA.equals(ListB) -> true
arrayA == arrayB -> false
arrayB == arrayC -> true
至于我们在设计可变类型ADT时,可能认为根据观察等价性是不错的选择。毕竟如果根据表现等价性,A与B相等只有可能A与B指向了同一块内存,也就是说不需要重写equals和hashcode,直接用Object提供的就行。但事实告诉我们,我们就应该根据表现等价性设计相等。也就是说,对于mutable类型的ADT,我们不应该重写Object类的equals()与hashCode()!下面是为什么这么做的原因:
List<String> list = new ArrayList<>();
list.add("a");
Set<List<String>> set = new HashSet<List<String>>();
set.add(list);
set.contains(list) → true
list.add("goodbye");
set.contains(list) → false!!!!
list改变后hashcode改变了,但是set中存放的hashcode并没有变,所以查找不到,但是通过iterator却发现它明明就在set中,这便是可变类型equals设计成观察等价性带来的bug,因为它只能保证在某一时刻两者是相等的
对于可变类型:
即,一定不要重写Object里的equals与hashCode方法
对于不可变类型:
即,一定要重写Object里的equals与hashCode方法