HashMap源码剖析

517 阅读4分钟

HashMap集合简介

HashMap特点:

  • 1.存取无序的
  • 2.键和值位置都可以是null,但是键位置只能是一个null
  • 3.键位置是唯一的,底层的数据结构可以控制键
  • 4.JDK1.8前数据结构是:链表 + 数组 。jdk1.8之后是 : 链表 + 数组 + 红黑树
  • 5.阈值(边界值) > 8 并且数组长度大于64才将链表转换为红黑树,变为红黑树的目的是为了高效的查询

HashMap中hash函数是怎么实现的?还有哪些hash函数的实现方式?

对于key的hashCode做hash操作,无符号右移16位然后做异或运算。还有平方取中法伪随机数法取余数法。这三种效率都比较低。而无符号右移16位异或运算效率是最高的

当两个对象的hashCode相等时会怎么样?

产生哈希碰撞,若key值内容相同则替换旧的value.不然连接到链表后面,链表长度超过阈值8就转换为红黑树存储。

何时发生哈希碰撞和什么是哈希碰撞,如何解决哈希碰撞?

只要两个元素的key计算的哈希码值相同就会发生哈希碰撞。jdk8前使用链表解决哈希碰撞。jdk8之后使用链表+红黑树解决哈希碰撞。

如果两个键的hashcode相同,如何存储键值对?

hashcode相同,通过equals比较内容是否相同

相同:则新的value覆盖之前的value

不相同:则将新的键值对添加到哈希表中

扩容机制

在不断的添加数据的过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。

为什么容量必须是2的n次幂?

确定元素在数组中的具体位置这个算法实际就是取模hash%length,计算机中直接求余效率不如位移运算。所以源码中做了优化,使用hash&(length-1),而实际上hash%length等于hash&(length-1)前提是length是2的n次幂。2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1。

什么时候才需要扩容

HashMap中的元素个数超过数组大小(数组长度)loadFactor(负载因子)时,就会进行数组扩容,loadFactor的默认值是0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中的元素个数超过16×0.75=12(这个值就是阈值或者边界值threshold值)的时候,就把数组的大小扩展为2×16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预知元素的个数能够有效的提高HashMap的性能。

HashMap中的其中一个链表的对象个数如果达到了8个,此时如果数组长度没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链表会变成红黑树,节点类型由Node变成TreeNode类型。当然,如果映射关系被移除后,下次执行resize方法时判断树的节点个数低于6,也会再把树转换为链表

HashMap的扩容算法

HashMap在进行扩容时,使用的rehash方式非常巧妙,因为每次扩容都是翻倍,与原来计算的 (n-1)&hash的结果相比,只是多了一个bit位,所以节点要么就在原来的位置,要么就被分配到"原位置+旧容量"这个位置。

因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就可以了,是0的话索引没变,是1的话索引变成“原索引+oldCap(原位置+旧容量)”。