HashMap源码浅析
jdk11,工具idea
一、存储结构
入口:Ctrl+N查找到hashmap源码,找到静态内部类
1 | /** |
初始化时,为一个Node类型的数组,每个元素为一组键值对。
1 | /** |
从静态类中的next
可以看出,Node为链表结构。即Node数组的每个元素(也可称为桶)都可存储一个链表。
从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。
二、拉链法
1. 源码跟踪
示例:创建一个hashmap,放入3个键值对。
- 新建一个HashMap,默认大小为16,且都为2的幂。
1 | /** |
- 将map中放入3个键值对。
1 |
|
1 | 2715 |
存入map的键值的哈希值并不等于取出时的哈希值
- 确定桶的下表位置(即数组下标),跟踪
put(k,v)
源码:
存入K1,V1
1 | public V put(K key, V value) { // K1,V1 |
- 计算k1的hash=2374
1 | static final int hash(Object key) { // k1 |
- put操作
1 | // 参数=2374,k1,v1,false,true |
- 后续操作很多源码从putVal展开
注意索引的取值方法 i = (n - 1) & hash,表示该位置原来没有桶时,新链表的位置
K1,K2,K3存入的hash值分别为2374,2375,2376,对应的下标依次为6,7,8
2. put方法
2.1 头插法
当桶下标相同时,后一个put的KV对插在前一个KV对的前面,即新链表插在旧链表的头部。
步骤:
计算K所在的桶下标
确定桶后,在链表在顺序查找,时间复杂度与链表长度成正比。
2.2 null值
在计算hash值时,空值处理 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);即hashmap可以插入null值,但null没有hashCode()方法,就无法计算桶下标,因此以第0个桶确定为null的位置。
3. 扩容
假设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。
为了降低复杂度,需要N/M尽可能大,因此M尽可能大。HashMap根据键(N)的数量动态调整table数组(M,Node<K,V>[] table)的长度,即动态扩容。
满足++size > threshold
条件时,进行扩容,调用resize()方法
1 | /** |
- 扩容时须要拷贝旧表到新表,因此很耗时
- 为提高扩容性能,处理碰撞,jdk1.8后,一个桶存储的链表长度大于 8 时会将链表转换为红黑树
1 | /** |