HashMap尚硅谷笔记

本文深入剖析了JDK1.8中HashMap的改进,包括引入红黑树解决链表过长问题,优化了遍历和查找性能。当链表长度超过8时,HashMap会将链表转换为红黑树,减少查找时间至O(logn)。此外,还详细介绍了扩容机制,通过2次幂的扩展和重新定位节点,有效解决了冲突并平衡了负载。
摘要由CSDN通过智能技术生成

深入分析 HashMap
一、传统 HashMap 的缺点
(1)JDK 1.8 以前 HashMap 的实现是 数组 + 链表,即使哈希函数取得再好,也很难达到元素
百分百均匀分布。
(2) HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个
时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n)
完全失去了它的优势。
(3) 针对这种情况, JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn) )来优化这个问题
二、 JDK1.8 HashMap 的数据结构
2.1HashMap 是数组 + 链表 + 红黑树( JDK1.8 增加了红黑树部分)实现的
新增红黑树
static final class TreeNode extends LinkedHashMap.Entry {
TreeNode parent; // red-black tree links
TreeNode left;
TreeNode right;
TreeNode prev; // needed to unlink next upon deletionboolean red;
}

2.2HashMap 中关于红黑树的三个关键参数
TREEIFY_THRESHOLD
一个桶的树化阈值
UNTREEIFY_THRESHOLD
一个树的链表还原阈值
static final int TREEIFY_THRESHOLD = 8
static final int
UNTREEIFY_THRESHOLD = 6

当桶中元素个数超过这个值时
需要使用红黑树节点替换链表节点
当扩容时,桶中元素个数小于这个值
就会把树形的桶元素 还原(切分)为链
表结构
MIN_TREEIFY_CAPACITY
哈希表的最小树形化容量
static final int MIN_TREEIFY_CAPACITY = 64
当哈希表中的容量大于这个值时,表中的桶才能进行树形化
否则桶内元素太多时会扩容,而不是树形化
为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
2.3HashMap JDK 1.8 中新增的操作:桶的树形化 treeifyBin()
Java 8 中,如果一个桶中的元素个数超过 TREEIFY_THRESHOLD( 默认是 8 ) ,就使用
红黑树来替换链表,从而提高速度。
这个替换的方法叫 treeifyBin() 即树形化。
//将桶内所有的 链表节点 替换成 红黑树节点
1 final void treeifyBin(Node[] tab, int hash) {
2 int n, index; Node e;
3 //如果当前哈希表为空,或者哈希表中元素的个数小于 进行树形化的阈值(默认为 64),
就去新建/扩容
4 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
5 resize();
6 else if ((e = tab[index = (n - 1) & hash]) != null) {
7 //如果哈希表中的元素个数超过了 树形化阈值,进行树形化
8 // e 是哈希表中指定位置桶里的链表节点,从第一个开始
9 TreeNode hd = null, tl = null; //红黑树的头、尾节点
10 do {
11 //新建一个树形节点,内容和当前链表节点 e 一致
12 TreeNode p = replacementTreeNode(e, null);
13 if (tl == null) //确定树头节点
14 hd = p;
15 else {
16 p.prev = tl;
17 tl.next = p;
18 }19 tl = p;
20 } while ((e = e.next) != null);
21 //让桶的第一个元素指向新建的红黑树头结点,以后这个桶里的元素就是红黑树而
不是链表了
22 if ((tab[index] = hd) != null)
23 hd.treeify(tab);
24 }
25 }
26 TreeNode replacementTreeNode(Node p, Node next) {
27 return new TreeNode<>(p.hash, p.key, p.value, next);
28 }

上述操作做了这些事 :
(1) 根据哈希表中元素个数确定是扩容还是树形化
(2) 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系
(3) 然后让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容
三、分析 HashMap put 方法
3.1HashMap put 方法执行过程可以通过下图来理解,自己有兴趣可以去对比源码更清楚
地研究学习。 . 判断键值对数组 table[i] 是否为空或为 null ,否则执行 resize() 进行扩容;
. 根据键值 key 计算 hash 值得到插入的数组索引 i ,如果 table[i]==null ,直接新建节点添加,
转向⑥,如果 table[i] 不为空,转向③;
. 判断 table[i] 的首个元素是否和 key 一样,如果相同直接覆盖 value ,否则转向④,这里的相
同指的是 hashCode 以及 equals
. 判断 table[i] 是否为 treeNode ,即 table[i] 是否是红黑树,如果是红黑树,则直接在树中插
入键值对,否则转向⑤;
. 遍历 table[i] ,判断链表长度是否大于 8 ,大于 8 的话把链表转换为红黑树,在红黑树中执
行插入操作,否则进行链表的插入操作;遍历过程中若发现 key 已经存在直接覆盖 value 即可;
. 插入成功后,判断实际存在的键值对数量 size 是否超多了最大容量 threshold ,如果超过,
进行扩容。
JDK1.8HashMap put 方法源码
1 public V put(K key, V value) {
2 // 对 key 的 hashCode()做 hash3 return putVal(hash(key), key, value, false, true);
4 }
5
6 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
7 boolean evict) {
8 Node[] tab; Node p; int n, i;
9 // 步骤①:tab 为空则创建
10 if ((tab = table) == null || (n = tab.length) == 0)
11 n = (tab = resize()).length;
12 // 步骤②:计算 index,并对 null 做处理
13 if ((p = tab[i = (n - 1) & hash]) == null)
14 tab[i] = newNode(hash, key, value, null);
15 else {
16 Node e; K k;
17 // 步骤③:节点 key 存在,直接覆盖 value
18 if (p.hash == hash &&
19 ((k = p.key) == key || (key != null && key.equals(k))))
20 e = p;
21 // 步骤④:判断该链为红黑树
22 else if (p instanceof TreeNode)
23 e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
24 // 步骤⑤:该链为链表
25 else {
26 for (int binCount = 0; ; ++binCount) {
27 if ((e = p.next) == null) {
28 p.next = newNode(hash, key,value,null);
//链表长度大于 8 转换为红黑树进行处理
29 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
30 treeifyBin(tab, hash);
31 break;
32 }
// key 已经存在直接覆盖 value
33 if (e.hash == hash &&
34 ((k = e.key) == key || (key != null && key.equals(k))))
35 break;
36 p = e;
37 }
38 }
39
40 if (e != null) { // existing mapping for key
41 V oldValue = e.value;
42 if (!onlyIfAbsent || oldValue == null)
43 e.value = value;
44 afterNodeAccess(e);45 return oldValue;
46 }
47 }
48 ++modCount;
49 // 步骤⑥:超过最大容量 就扩容
50 if (++size > threshold)
51 resize();
52 afterNodeInsertion(evict);
53 return null;
54 }

JDK1.7HashMap put 方法源码
1 public V put(K key, V value) {
2 if (table == EMPTY_TABLE) { //空表 table 的话,根据 size 的阈值填充
3 inflateTable(threshold);
4 }
5 if (key == null)
6 return putForNullKey(value);
7 int hash = hash(key);//成 hash,得到索引 Index 的映射
8 int i = indexFor(hash, table.length);
9 for (Entry e = table[i]; e != null; e = e.next) {//遍历当前索引的冲突链,
找是否存在对应的 key
10 Object k;
11 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//如果存
在对应的 key, 则替换 oldValue 并返回 oldValue
12 V oldValue = e.value;
13 e.value = value;
14 e.recordAccess(this);
15 return oldValue;
16 }
17 }
18 //冲突链中不存在新写入的 Entry 的 key
19 modCount++;
20 addEntry(hash, key, value, i);
21 return null;
22 }

3.2HashMap JDK 1.8 中新增的操作: 红黑树中查找元素 getTreeNode()
JDK1.8 hashMap getNode 操作
*/
1 final Node getNode(int hash, Object key) {
2 Node[] tab; Node first, e; int n; K k;
3 if ((tab = table) != null && (n = tab.length) > 0 &&
4 (first = tab[(n - 1) & hash]) != null) {
5 if (first.hash == hash && // always check first node6 ((k = first.key) == key || (key != null && key.equals(k))))
7 return first;
8 if ((e = first.next) != null) {
9 if (first instanceof TreeNode)
10 return ((TreeNode)first).getTreeNode(hash, key);
11 do {
12 if (e.hash == hash &&
13 ((k = e.key) == key || (key != null && key.equals(k))))
14 return e;
15 } while ((e = e.next) != null);
16 }
17 }
18 return null;
19}

(1)HashMap 的查找方法是 get(), 它通过计算指定 key 的哈希值后,调用内部方法 getNode()
(2) 这个 getNode() 方法就是根据哈希表元素个数与哈希值求模( 使用的公式是 (n - 1)
&hash )得到 key 所在的桶的头结点,如果头节点恰好是红黑树节点,
就调用红黑树节点的 getTreeNode() 方法,否则就遍历链表节点。
(3)getTreeNode 方法使通过调用树形节点的 find() 方法进行查找:
1 final TreeNode getTreeNode(int h, Object k) {
2 return ((parent != null) ? root() : this).find(h, k, null);
3 }
(4) 由于之前添加时已经保证这个树是有序的,因此查找时基本就是折半查找,效率很高。
(5) 这里和插入时一样,如果对比节点的哈希值和要查找的哈希值相等,就会判断 key 是否相
等,相等就直接返回;不相等就从子树中递归查找。
3.3JDK1.8 VS JDK1.7 扩容机制
下面举个例子说明下扩容过程。假设了我们的 hash 算法 就是简单的用 key mod 一下表的大
小(也就是数组的长度)。其中的哈希桶数组 table size=2 , 所以 key = 3 7 5 put
序依次为 5 7 3 。在 mod 2 以后都冲突在 table[1] 这里了。这里假设负载因子 loadFactor=1
即当键值对的实际大小 size 大于 table 的实际大小时进行扩容。接下来的三个步骤是哈希桶
数组 resize 4 ,然后所有的 Node 重新 rehash 的过程。 下面我们讲解下 JDK1.8 做了哪些优化。经过观测可以发现,我们使用的是 2 次幂的扩展 ( 指长
度扩为原来 2 ) ,所以,元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置。
看下图可以明白这句话的意思, n table 的长度,图( a )表示扩容前的 key1 key2 两种
key 确定索引位置的示例,图( b )表示扩容后 key1 key2 两种 key 确定索引位置的示例,
其中 hash1 key1 对应的哈希与高位运算结果。
元素在重新计算 hash 之后,因为 n 变为 2 倍,那么 n-1 mask 范围在高位多 1bit( 红色 ) ,因
此新的 index 就会发生这样的变化: 因此,我们在扩充 HashMap 的时候,不需要像 JDK1.7 的实现那样重新计算 hash ,只需要看
看原来的 hash 值新增的那个 bit 1 还是 0 就好了,是 0 的话索引没变,是 1 的话索引变成
索引 +oldCap” ,可以看看下图为 16 扩充为 32 resize 示意图:
这个设计确实非常的巧妙,既省去了重新计算 hash 值的时间,而且同时,由于新增的 1bit
0 还是 1 可以认为是随机的,因此 resize 的过程,均匀的把之前的冲突的节点分散到新的 bucket
了。这一块就是 JDK1.8 新增的优化点。
HashMap中调用size()方法返回的是entry键值对的个数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值