要彻底理解重写equals()为什么必须重写hashcode()方法,就需要明白HashMap的put过程以及什么是Hash冲突。
首先我们来说一下HashSet与HashMap:
我们可以从源码中知道:
HashSet的实现其实就是HashMap的key,其所有的Value是PRESENT(源码中写死的new的一个Object对象),而我们往HashSet添加值的时候(add()),其实是给底层HashMap添加值,源码如下:
public boolean add(E e) { return map.put(e, PRESENT)==null; }
map.put(e,PRESENT)中的map跟PRESENT在第一张图中其实都已经标出来了。
接下来我们探讨HashMap的底层结构以及put原理
HashMap的底层是以数组加链表实现的,附上到网上找的一张图:
HashMap是数组加链表的形式存储数据的,数组的初始长度是16,HashMap自带扩容机制,HashMap的容量(我的理解是数组长度)范围是在16(初始化默认值)~2 ^ 30;
扩容机制:每次长度增加一倍,初始化长度是16,这也解释了为什么HashMap的长度总是2的次方;
HashMap的put()过程:
这个问题跟为什么重写hashCode()息息相关,重点:
如果往HashMap中put()一个key-value,首先根据key的hashCode()方法计算出一个hashCode值,这个值再通过hash()方法,计算得出这个key-value应该放在数组中的哪个位置(即数组下标),这也是我所理解的再哈希法(如果有人去了解如何解决hash冲突的时候应该会明白这个字眼),找到数组下标后,我们知道这个数组里面的每一个元素都是一个链表,这个时候就采用key的equals()方法去此链表中一个个去比对,看链表中是否有相同的key,如果有相同的key,则覆盖,如果没有相同的key,则在链表末尾添加;
上面就是我所理解的HashMap的put()的大概过程,可能会有一些地方有遗漏;
为什么不直接用hashcode值直接作为数组下标:因为hashCode值最大有2的31次方,数组好像没有这么大长度,hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置(数组下标);
hashCode()与equals()
hashCode()跟equals()都是都是从超级父类Object中继承而来的方法,在Object中他们分别是:
public native int hashCode();
public boolean equals(Object obj) { return (this == obj); }
hashCode()方法不重写的话,是用native修饰的,也就是默认由操作系统实现,即hashcode值默认是由通过内存中的引用计算得出的;
equals()方法不重写的话,底层是用"=="来判断是否相等,"=="是判断引用是否相等;
重写了equals()为什么必须重写hashcode()
举个例子:
有一个类(定位A)有两个属性:String name,int id
现在我们new了两个对象,都是new A(srg,123);也就是说这两个对象的属性值是相等的,但是明显这两个对象的引用不相等。
通常针对大部分场景时,我们只要确定他们的属性值相等,就认为他们相等。
现在我们在A类中重写了equals()方法,我们重写equals()的判断规则如下:
public boolean equals(A a) { if(this.name.equals(a.name) && this.id==a.id){ return true; }else{ return false; } }
name跟id是String跟int类型的,int是基础数据类型,==是用来判断值是否相等;String是引用类型,但是String类Java已经帮我们重写了equals()方法,也是判断值相等。
(注意:==对于值类型判断值,对于引用类型判断引用)
所以我们刚刚重写equals()方法的规则是只要A类对象的值相等,调用equals()就返回true,否则返回false。
但是我们new的两个对象new A(srg,123),虽然他们equals()为true,但是明显它们hashCode值不相等(上面已经说过:因为引用不一样,而hashCode()是操作系统根据引用来计算的);
理解了上述的东西的话,我们又回到HashMap的put()过程,我们首先通过hashCode()方法计算下标,上面new的两个对象new A(srg,123),在A类中没有重写hashCode方法的话,如果把这个两个对象作为HashMap中的key,是不符合HashMap的结构设计的(key不能重复);
为什么呢?因为通过计算hashCode()跟hash()计算,这个两个对象如果作为key的话,他们在数组中的下标是不一样的,假设A1对象在下标1中,A2对象在下标23中,A1首先put()进去,然后在A2对象put()时,它到下标23中与其链表里面的每一个元素通过equals()方法判断是否相等,发现没有相等的,然后也put()进去了。
这个时候,此HashMap中有两个key一样的key-value,分别在下标1中的A1,以及下标23中的A2,很明显,这不符合我们设计的HashMap的初衷。
所以当A类对象只重写了equals()方法,而没有重写hashCode()方法的话,此对象不能作为HashMap的Key,也不能放入Set集合里,大家也可以仔细想想,那你设计的这个数据结构还能做什么。
通常我们一般以String或者Integer等作为HashMap的key,因为他们已经都重写equals()跟hashCode()方法;
最终总结:重写了equals()为什么必须重写hashcode(),这只是一种数据结构设计的规范,我们都应该遵守这种规范。