HashMap底层源码解析
1.底层数据结构:哈希表
我们常见的数据结构有三种:
- 数组结构
- 链表结构
- 哈希表结构
HashMap的底层数据结构是哈希表:
-
数组+链表 同一位置下,节点数<=8;
- 数组+红黑树 同一个位置下,节点的个数>8 并且 数组长度大于64;
补充(hash冲突):
- 当两个key通过hashCod计算相同时(其实hashCode是随机产生的,是有可能hashCode相同,因为哈希表是有限的,但存入的东西可能很多难免有些会有相同的hashCode,从而发生冲突。例如:"通话"和"重地"),则发生了hash冲突
- HashMap解决hash冲突的方式是用链表。当发生hash冲突时,则将存放在数组中的Entry设置为新值的next,举例来说,比如A和B都hash后都映射到下标i中,之前已经有A了,当map.put(B)时,将B放到下标i中,A则为B的next,所以新值存放在数组中,旧值在新值的链表上
- 主要的解决冲突的方法有:开放定址法、再哈希法、链地址法、建立公共溢出区
2.HashMap中添加元素和查找元素
HashMap中添加元素方法为map.put(k,v);查找元素的方法为map.get(k);
情景实例:(tips:可以按住Ctrl+鼠标点击方法名的方式进入方法体里)
package Test;
import java.util.HashMap;
public class Demo02 {
public static void main(String[] args) {
//假设一个情景,使用hashmap集合录入一批人姓名及其所具有的宝石数
HashMap<String, Integer> hashMap=new HashMap<String, Integer>();
hashMap.put("张三", 6);
hashMap.put("李四", 7);
hashMap.put("王五", 8);
int m=hashMap.get("李四");
System.out.println("李四所具有的宝石数为:"+m);
System.out.println("hashMap集合:"+hashMap);
}
}
2.1、源码中的map.put(k,v)方法
2.1.1、求元素的哈希值
从map.put()方法进入后可以看到源码中的put()方法,传入了键key和值value.在进入下一个putVal()方法前先执行了hash()方法,判断了传入键的哈希值;进入hash()方法可以看到一个三元运算符,其表达的意思是若传进来的key为null返回0,否则对其进行hashCode(),拿到对应的哈希值然后和0进行异或运算。
补充:
- h>>>16:如果元素(key)的哈希值是在65535的范围内,那么该结果都为0
- 任何数异或(^)0,还是原来的数
2.1.2、添加元素(map集合为空时)
进入putVal这个添加元素的主要方法中后,可以看到会进行创建一个tab数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;进入第一个if判断时,若map集合中没有元素,会进入resize()方法。
进入resize()方法后可以观察到, int oldCap = (oldTab == null) ? 0 : oldTab.length;
这样就创建了一个容量为16的map集合,可以将元素添加到map集合中。
2.1.3、确定元素位置(在数组中那个索引位置)
总结:新增的对应位置(索引值): 哈希值%16
2.1.4、新增元素
若该索引位置有节点 但hash值不同,获取头结点的下一个节点,判断是否为空:
for(;;){
如果为空:直接把该节点挂到头结点的后面
如果不为空:下一个节点和要添加的元素进行操作。
p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))):
如果该结果为true,则不新增。
否则:继续上面的循环一次走。
}
2.1.5、如何扩容
2.1.6、扩容后哈希表中元素变化
2.1.7、小结
map.put(k,v)实现原理:
- 首先将k,v封装到Node对象当中(节点)
- 然后它的底层会调用K的hashCode()方法得出hash值
- 通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
2.2 源码中的map.get(k)方法
2.2.1、源码
2.2.2、小结
map.get(k)实现原理:
先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标
通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。
3.HashMap的原理1.7和1.8的区别
区别:
- jdk1.7中底层是由数组+链表实现;
- jdk1.8中底层是由数组+链表/红黑树实现
优缺点:
- 可以存储null键和null值,线程不安全
- 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
- 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
- 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
tips:你的点赞+关注是小编更新的动力!