文章目录
hashCode 的常规协定
查看官方文档,可以看到 hashCode
的常规协定是:
- 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
- 如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须相同的整数结果。
- 如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法 不要求 一定生成不同的整数结果(存在哈希冲突)。
原生hashCode、equals方法
- Java 里面任何一个对象都有一个
native
的hashCode()
方法,默认是JVM使用随机数来生成的(满足上面的协议)。
- 在Java中,如果我们没有自定义equals方法,那么默认的equals方法就是比较两个对象的 地址是否相同
散列集合的重复性校验
散列集合(HashSet、HashMap等等)添加元素会有重复性校验,依据的就是上面的协议,具体校验的方式如下:
先使用对象的hashCode的值进行取模运算,判断是否相等:
- 不同,将直接判定 Object 不同,跳过equals方法的检验,这加快了冲突处理效率。
- 相同,这个位置通过哈希算法可能存在多个元素(存在哈希冲突),这个时候即需要通过 equals方法比较(极大缩小比较范围,高效判断),最终判定该存储结构中是否有重复元素。
举例说明:
为了更好地理解这个问题,这里举一个具体的例子:
定义一个学生类,并在main方法中创建两个对象s1和s2,将s1加入到map中,然后通过s2获取值。
public class Student {
private Integer id;
public Student(Integer id) {
this.id = id;
}
public static void main(String[] args) {
Student s1 = new Student(1);
Student s2 = new Student(2);
Map<Student,String> map = new HashMap<>();
map.put(s1, "Hello World!");
System.out.println(map.get(s2)));
}
}
1. 假设没有重写hashCode方法
native的hashCode()方法由于s1和s2是两个不同的对象,对应的 地址值不同,根据hashCode计算出的 哈希值不同,它们将散列在哈希表中的不同位置。所以就无法通过s2获取s1对应的值。
输出结果为 null
。
2. 如果重写了hashCode方法
@Override
public int hashCode() {
return Objects.hash(id);
}
此时 s1 和 s2 都是通过调用属性 id
的 hashCode 方法来计算哈希值,由于Integer类中已经重写了hashCode方法(见下),s1.hashCode() == s2.hashCode
,也就是说它们都散列在哈希表的同个位置。
但是在哈希式的存储结构中存在哈希冲突,因此还需要使用equals
方法进一步判断。
/**
* Returns a hash code for this <code>Integer</code>.
*
* @return a hash code value for this object, equal to the
* primitive <code>int</code> value represented by this
* <code>Integer</code> object.
*/
public int hashCode() {
return value;
}
3. 假设重写了hashCode方法,但是没有重写equals方法:
在java中,map集合底层是通过 数组+链表+红黑树
实现的(即:使用“拉链法”解决哈希冲突)。
由于重写 hashCode
方法后,s1和s2计算出的哈希值相同(哈希冲突),就会继续使用equals方法,判断对应链表中是否存在s2元素。
由于没有重写 equals
方法,equals方法默认还是通过比较内存地址 判断两个元素是否相同,由于s1和s2的地址值不同,s1.equals(s2) == false
,因此还是无法获取s1对应的值。
输出结果仍然是null
;
4. 如果重写了hashCode方法,并且重写了equals方法:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(id, student.id);
}
s1和s2是通过调用 id
的equals方法来比较是否相同,由于Integer内部已经重写了equals方法(如下),s1.equals(s2) == true
,可以通过s2获取s1对应的值。
/**
* Integer源码
* A.equals(B) 比较两个对象A和B。
* 当且仅当B非空,而且B是个值和A的值相等的Integer对象时,A和B相等。
*/
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
输出结果为:Hello World!
问题回答
如果类中重写了 equals
方法,但没有重写 hashCode
方法,会出现创建出的两个对象,a.equals(b)
这个表达式成立,但是生成的哈希值不同的悖论 —— 由于散列集合是通过 hashCode
计算 key 的存储位置,也就是说这两个完全相同的对象,却存储在哈希表中的两个位置,与协议相违背,程序会出现不可预料的错误。因此重写 equals
方法就一定要重写 hashCode
方法。