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

字数1,730 大约花费7分钟

目录

  1. 1. hashCode 需满足的条件
  2. 2. 如何计算 hashCode
  3. 3. 如何计算每个重要字段的值
  4. 4. 计算 hashCode 的注意事项
  5. 5. TantanitReaderPhone 的哈希值计算

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
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
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 的代码,亲自试试。

谈谈 IT的文章均为原创或翻译(翻译会注明外文来源),转载请以链接形式标明本文地址: http://tantanit.com/java-always-override-hashcode-when-override-equals/

谈谈IT

欢迎关注官方微信公众号获取最新原创文章