高效Java技巧重写equals方法时应当重写hashCode方法

hashCode需满足的条件

  • 当equals方法中涉及的参数没有改变时,hashCode应保持不变
  • 如果根据equals方法,两个对象是相等的,那么这两个对象的hashCode应该一样
  • 两个对象如果不相等,hashCode不强制要求不一样,但是如果能保证不一样,对哈希的效率会比较有帮助

最重要的是第二点,相等的对象必须有相同的hashCode,由于默认的hashCode方法针对每一个对象返回一个固定的随机值(有的实现是根据对象地址返回值,相当于每一个对象对应一个固定的随机值),所以当我们使用equals方法的同时,必须override(重写)hashCode方法,以满足这一点。

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
39
40
41
42
43
public class TantanitReaderPhone {
private String areaCode;
private String localNumber;

public TantanitReaderPhone(String areaCode, String localNumber) {
this.areaCode = areaCode;
this.localNumber = localNumber;
}

@Override
public int hashCode() {
return super.hashCode();
}

@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (!(obj instanceof TantanitReaderPhone))
return false;
TantanitReaderPhone tantanitReaderPhone = (TantanitReaderPhone)obj;
return areaCode.equals(tantanitReaderPhone.areaCode)
&& localNumber.equals(tantanitReaderPhone.localNumber);
}

public static void main(String[] args) {
Map<TantanitReaderPhone, String> tantanitReaderPhoneStringMap
= new HashMap<>();
tantanitReaderPhoneStringMap.put(
new TantanitReaderPhone("86","13200001234"),"张三"
);
String name=tantanitReaderPhoneStringMap.get(
new TantanitReaderPhone("86","13200001234")
);
if(name==null){
System.out.print("name is null");
}else {
System.out.print(name);
}

}

}

上面的代码是一个手机号码的例子,手机号码由区号(比如中国是86)和本国手机号构成。我们重写了equals方法,但hashCode使用的仍然是父类也就是Object类的方法,可以理解为是一个随机数。在main函数中,我们定义了一个以TantanitReaderPhone为key的hashMap,保存并试图取出一个value值。需要注意的是,我们保存和取出时,使用的是两个不同的对象(两次都是new一个新的对象),但两个对象有着相同的areaCode和localNumber,根据我们重写的equals方法,这两个对象是相等的。但是由于我们没有重写hashCode方法,这两个对象的哈希值不同,所以使用第二个对象无法在hashMap里找到第一次存进去的值。

这是因为,哈希表的每个分区,只会对应有限的哈希值,并存储这些哈希值对应的对象。所以哈希值不同,首先找不到对应的分区,即使碰巧哈希表有分区同时对应着两个哈希值,由于哈希表往往会进行优化,对哈希值先进行判断,所以不相等的哈希值找不到对应的对象。

所以,当需要根据哈希值进行存储时,应该重写hashCode方法,根据字段值生成对应的hashCode(哈希值)。下面讲解如何计算hash值,并且改写我们上面的例子,重写hashCode方法,再执行以下main函数,看看有什么结果。

如何计算hashCode

  1. 生成一个int类型的变量result,并且初始化一个值,比如17
  2. 对类中每一个重要字段,也就是影响对象的值的字段,也就是equals方法里有比较的字段,进行以下操作:
    a. 计算这个字段的值filedHashValue
    b. 执行 result = 31 * result + filedHashValue; 更新结果

而要如和计算这个字段的值filedHashValue值呢,根据字段类型分为三种情况,一种是基础数据(比如int,boolean,),一种是对象,还有一种是数组。

如何计算每个重要字段的值

如果字段是基础数据,假设数值为f,根据类型

  • boolean,true返回1,false返回0
  • byte, char, short, int返回对应的int值
  • long,返回f^(f>>>32)
  • float,返回Float.floatToIntBits(f)
  • double,执行Double.doubleToLongBits(f)转为long,再返回compute(int)(f^(f>>>32))

而如果字段是对象,如果是null,返回0,否则根据上述【如何计算hashCode】中所述步骤1和2,计算这个对象的hashCode,作为这个字段的值。实际上是一个递归的过程。

而如果字段是数组,则对每一个元素进行计算,得到filedHashValue,并执行result = 31 * result + filedHashValue;

这里补充一下HashMap对象是如何计算哈希值的,JDK中,HashMap类自身已经实现了hashCode方法。一个hashMap其实是entry的数组。
AbstractMap中,hashCode对数组中的每一个entry计算哈希值,并得到所有哈希值的和。

1
2
3
4
5
6
7
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode();
return h;
}

而每一个entry按照以下方式计算哈希值(JDK1.7之后的版本)。

1
2
3
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

这个实现非常巧妙,由于entry由key和value生成,所以将两个哈希值求与,既保证具有相同key和value的entry一定具有相同的哈希值,又保证了效率(与操作的效率很高)。

计算hashCode的注意事项

  • 不能包含equals方法中没有的字段,否则会导致相等的对象可能会有不同的哈希值
  • 和equals方法一样,不应当包含可以通过其他字段计算出来的字段
  • 不要尝试减少计算重要字段,虽然这样做,在算hash值时会比较快,但会导致同一个hash值对应过多对象,比如TantanitReaderPhone中如果只使用areaCode字段计算哈希值,那么所有来自中国的手机号都会是同一个hash值,显然哈希表的性能会很低。
  • 如果对象的值不可更改,应当考虑延迟计算并且缓存哈希值,比如JDK中String的值是不可变的(每一次更改值都会生成新的String对象),所以对hash值的计算使用了延迟计算,并且缓存,String的hashCode方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;

for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

TantanitReaderPhone的哈希值计算

1
2
3
4
5
6
7
@Override
public int hashCode() {
int result = 17;
result = 31 * result + areaCode.hashCode();
result = 31 * result + localNumber.hashCode();
return result;
}

由于areaCode和localNumber都是用来区分TantanitReaderPhone的重要字段,所以根据这两个字段来计算哈希值。这两个字段都是String类型,直接调用String自带的hashCode方法(areaCode和localNumber假定都不为null)。

将TantanitReaderPhone的hashCode方法按照上述代码进行重写后,再执行main函数,打印出“张三”,成功从hashMap中取出对应的值了。您可以复制一下TantanitReaderPhone的代码,亲自试试。

© 2022 谈谈IT All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero