我是针对在元素被存入哈希表时的流程分析的
默认实现
首先要明确,hashCode和equals这两个方法Object中的默认实现
- hashCode方法
@HotSpotIntrinsicCandidate
public native int hashCode();
Object中,hashCode方法是被native修饰的,说明底层调用了c或c++的内容。其实做的事情是:根据对象实际的物理内存地址,通过某种算法得出一个数值。
- equals方法
public boolean equals(Object obj) {
return (this == obj);
}
equals方法的默认实现是:只有两个对象的保存的地址值是同一个时,才判断其相等,也就是说,两个引用指向堆内存中同一个对象,才判断相等。
数组、链表和哈希表
了解了hashCode和equals方法的默认实现了之后,我们再来看一看数组、链表和哈希表的特点
数组
数组的特点:查找快,增删慢。
数组就像一列有序的格子,格子是连续的。
因为数组空间是连续的,而每个个元素所占的内存大小也是固定的,知道下标,就可以通过简单的数学表达式就可以计算出元素的内存地址,从而迅速定位到元素。
而增删慢的原因是:当想插入或删除某个元素到某个位置时,那么这个位置后面的每个元素的地址都要改变,还涉及到扩容问题。就像一排连续的被编好号的格子,每个格子里面都放了东西,现在你又添置了物品,想放在他们中间。
你能直接新加一个格子在他们中间,然后将新添置的物品放进去吗?这是不行的,因为之前的格子都被编好号了,所以是无法在他们之间新增一个格子,只能在最后加一个格子,接着最后的号排上号,然后挨个把物品往后挪,腾出前面的那个空格子,再将新购置的物品放进去。
链表
链表的特点,和数组相反。增删快,查找慢
链表就像一列人,手拉着手,每个人是一个节点。
链表的内存地址不是连续的,但是它的每个元素的记录了下一个元素的地址(单链表)。
增删快,因为如果要增加或者要删除或者要增加为一个元素,只需要将两个节点中保存的下一个节点修改一下就行了。就像一群手拉着手的小朋友在做游戏,然后新来了一个小朋友要加入他们,无论新加到那个位置,只需要让两个小朋友的手分开,然后把新来的那个小朋友放在他们中间,然后再重新让他们牵手,就行了,删除也是类似。
查找慢,因为链表查找,不能直接定位到内存地址,只能挨个遍历,然后比较是否相等。
哈希表
哈希表为什么性能高呢?因为它综合了数组和链表两者的有点,哈希表是由数组+链表/红黑树组成的,查找和增删的效率都很优秀。
链表的地址是就是通过哈希值计算得出的,如果哈希值相同,就将元素放在哈希值相等的一个链表/红黑树中。
哈希表存元素的步骤是:先通过hashCode方法得到这个元素的哈希值,再有这个哈希值计算得到一个内存地址值,然后再去看该内存地址上有没有元素,如果没有,就放在那里,如果已经有了,就去调用equals方法,看和已经存在的元素是否“相等”,如果相等,就舍去,如果不等,就将这个元素挂在已经存这个元素的链表(或红黑树)上
假设
现在来说为什么要同时重写hashCode和equals方法。
-
假设不重写hashCode方法
不重写hashCode方法,意味着,每次将元素存入哈希表时,哈希值都是通过Object类中默认的的hashCode方法得到的,因为默认的实现又是通过对象的地址计算得到的。所以每次每个对象得到的哈希值大概率不相同(我不知道是不是一定不同)。那么在通过哈希值计算数组的地址时,也全都是不同的地址,就这个哈希表就和数组没什区别了,不能发挥出哈希表的性能和优势。 -
假设不重写equals方法
这个得看hashCode方法有没有重写,如果hashCode方法没有重写,那么每次对象得到的哈希值不同,所以根本不会轮到equals方法出场,重写不重写都差不多。
如果是重写了hashCode方法,那么在两个元素哈希值相同时,就会去调用Object中equals的默认实现,默认实现是啥?两个引用指向同一个对象时,才会判断相等。如果两个对象的内容是相同的话,也会被equals方法判断为不同,那么还是会被存入哈希表,而我们通常的认知是认为内容相同的对象就是相同的,不应该被存入哈希表。然而实际上却存入了,违背了哈希表不能存入重复元素的原则。