1、阿里巴巴开发手册

在阿里巴巴开发手册(六)-集合处理中,可以看到这条强制规约

image-20210311214636678

2、查看底层源码

hashCodeequalsObject类下的两个方法,其中hashCode 方法以 native 关键字修饰,这个方法并非Java实现。

equals 方法底层调用了 == 来与传入的对象做比较(比较两个对象地址值)

3、hashCode介绍

hashCode方法是每个类中都拥有的一个函数,主要返回每个对象的hash值,hash值主要用于散列表(HashMap、HashSet、HashTable)中。

对hash码的通用约定如下:

  • 从Hash值不能反向推导出原始的数据
  • 输入数据的微小变化会得到完全不同的Hash值,相同的数据会得到相同的值。

在java程序执行过程中,在一个对象没有被改变的前提下,无论这个对象被调用多少次,hashCode方法都会返回相同的整数值。对象的哈希码没有必要在不同的程序中保持相同的值。

  • Hash算法的执行效率要高效,长的文本也能快速计算出Hash值
  • 如果2个对象使用equals方法进行比较并且相同的话,那么这2个对象的hashCode方法的值也必须相等。
  • 如果根据equals方法,得到两个对象不相等,那么这2个对象的hashCode值不需要必须不相同。但是,不相等的对象的hashCode值不同的话可以提高哈希表的性能。

4、案例

4.1、编写一个Person类

这里不重写hashCode方法,只重写Person类的equals方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
public class Person {
private Integer age;
private String name;

@Override
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if(obj instanceof Person) {
Person person = (Person)obj;
return this.age.equals(person.age) && this.name.equals(person.getName());
}
return false;
}
}

编写一段测试代码,测试两个满足equals方法的Person对象的hashCode是否相等

1
2
3
4
5
Person p1 = new Person(1,"小明");
Person p2 = new Person(1,"小明");
System.out.println("p1与p2使用equals方法判断是否相等的结果是:" + p1.equals(p2));
System.out.println("对象p1的hashCode为:" + p1.hashCode());
System.out.println("对象p2的hashCode为:" + p2.hashCode());

执行结果

image-20210311221800697

从上面运行的结果可以看出,如果只重写equals方法,不重写hashCode方法,那么使用equals方法判断为相等的两个对象的hash值可能不同。

4.2、向HashSet中添加不重写hashCode方法的对象

HashSet底层是维护了一个HashMap对象,在往HashSet对象中add对象时,实际上调用了底层HashMap对象的put方法,传入的Entry对象中,键为待添加对象,值固定为一个Object对象

image-20210311223030452

add方法

image-20210311223322917

  • 创建一个HashSet对象,使用Person作为泛型,然后添加几个Person对象
1
2
3
4
5
6
7
8
Person p1 = new Person(1,"小明");
Person p2 = new Person(1,"小明");
Person p3 = new Person(2,"小明");
Set<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println(set);

运行结果

image-20210311223922525

可以看到,在重写equals方法而不重写hashCode方法的情况下,如果将该类作为散列表的键或者HashSet的泛型,就会造成出现多个一模一样的key或者对象。

4.3、重写Person类的hashCode方法

将Person对象的name属性全部转换为大写,然后使用String类的hashCode方法计算name的hash值,然后与Person对象的age进行异或运算。

1
2
3
4
5
@Override
public int hashCode() {
int nameCode = name.toUpperCase().hashCode();
return nameCode ^ age;
}

4.4、再次运行上面的两个案例

1
2
3
4
5
Person p1 = new Person(1,"小明");
Person p2 = new Person(1,"小明");
System.out.println("p1与p2使用equals方法判断是否相等的结果是:" + p1.equals(p2));
System.out.println("对象p1的hashCode为:" + p1.hashCode());
System.out.println("对象p2的hashCode为:" + p2.hashCode());

运行结果,此时可以看到使用equals比较结果为true的两个对象的hashCode一定相等。

注意:此时hashCode值相等的对象equals后的结果不一定为true

image-20210311225155026

1
2
3
4
5
6
7
8
Person p1 = new Person(1,"小明");
Person p2 = new Person(1,"小明");
Person p3 = new Person(2,"小明");
Set<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println(set);

执行结果,此时可以看到HashSet对象中没有两个相同的对象

image-20210311230850503

5、总结

5.1、对于不会创建散列表的类

“不会创建散列表”的意思是:

我们不会在HashSetHashMapHashTableConcurrentHashMap等等这些本质是散列表的数据结构中使用这个类,其中HashMapHashTableConcurrentHashMap中不会作为键使用。

在这种情况下,hashCodeequals方法不存在关系,此时equals方法用于判断两个对象是否相等,而hashCode全无作用。

5.2、对于会创建散列表的类

在这种情况下,我们必须重写hashCodeequals方法,并且重写的hashCode方法必须满足上面给出的hash通用约定。

在这种情况下,hashCode方法和equals方法得出的结果有如下关系:

  • 如果两个对象使用equals方法比较后的结果为true,那么这两个对象经由hashCode方法得到的结果一定一致。
  • 如果两个对象使用equals方法比较后的结果为false,那么这两个对象经由hashCode方法得到的结果可能相等。(在HashMap中,两个hashCode相同而键不同的现象称为hash冲突)