特性
- HashMap 是一个采用哈希表实现的键值对集合,继承自 AbstractMap,实现了 Map 接口
- HashMap 的底层实现是 数组+链表,JDK 8 后又加了 红黑树
- 实现了 Map 全部的方法
- key 用 Set 存放,所以想做到 key 不允许重复,key 对应的类需要重写 hashCode 和 equals 方法
- 允许空键和空值(但空键只有一个,且放在第一位,下面会介绍)
- 元素是无序的,而且顺序会不定时改变(下面会介绍)
- 插入、获取的时间复杂度基本是 O(1)
- 遍历整个 Map 需要的时间与 桶(数组) 的长度成正比(因此初始化时 HashMap 的容量不宜太大)
- 两个关键因子:初始容量、加载因子
- HashTable除了不允许 null 并且同步,其他和HashMap几乎一样
属性
hash(Object key)原理,为什么(hashcode >>> 16)
参考:https://blog.csdn.net/qq_42034205/article/details/90384772
容量为什么必须是2的幂?
最大容量为什么是2的30次方,而不是31次方?
我们知道int类型是4个字节共32位,按理说应该可以向左移动31位,即2的31次方,为什么是30次方呢,原因就是由于二进制数字中最高的一位也就是最左边的一位是符号位,用来表示正负之分(0为正,1为负),所以只能向左移动30位,而不能移动到处在最高位的符号位!
在Java 8 中,如果一个桶中的元素个数超过 TREEIFY_THRESHOLD(默认是 8 ),就使用红黑树来替换链表,从而提高速度。
这个替换的方法叫 treeifyBin() 即树形化。
为什么元素顺序会不定时改变?
参考:https://blog.csdn.net/wenjianfeng/article/details/91348977
当map的容量饱和时,会进行resize(),创建一个新的Entry空数组,长度是原数组的2倍,遍历原Entry数组,把所有的Entry重新Hash到新数组
为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。
hash公式:index = HashCode(Key) & (Length - 1)
hashMap为什么会出现死循环?
重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。
方法实现
追加(put)
- 根据key获取hash值
- 底层存储数据的数组tab为空,进行第一次扩容,同时初始化tab
- 计算要存储数据的下标index,如果当前位置为null,直接插入
- tab[]首个元素就是是否位null,
- 是:直接插入
- 否:key是否存在
- 是:直接覆盖
- 否:table[i]是否位treeNode
- 是:红黑树直接插入键值对
- 否:遍历链表准备插入,链表长度是否大于8
- 是:转换红黑树,插入键值对
- 否:链表插入,若key存在,直接覆盖value
- modCount++
- count> threshold 当前容量大于 阀值需要扩容
移除(remove)
根据key删除对应的value
- 根据key获取hash值
- 判断table和hash对应的桶是否为空
- 判断要删除的节点是不是头节点
- 如果不是头节点,获取下以节点,并判断头节点是树节点还是链表节点
- 树节点:进入树查找要删除打的节点
- 链表节点:遍历链表,hash值相等,并且key地址相等或者equals
- ++modCount;
- --size;
- 返回node
- 匹配不到返回null
补充:
put图形介绍: