Skip to content

Commit d45fdb3

Browse files
committed
[docs update]结合源码分析一下 HashMap 链表到红黑树的转换。
1 parent b81ae5b commit d45fdb3

File tree

2 files changed

+63
-5
lines changed

2 files changed

+63
-5
lines changed

docs/about-the-author/zhishixingqiu-two-years.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@ star: 2
6060

6161
简单统计了一下,到目前为止,我至少帮助 **4000+** 位球友提供了免费的简历修改服务。
6262

63+
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/xingqiu/%E7%AE%80%E5%8E%86%E4%BF%AE%E6%94%B92.jpg)
64+
6365
我会针对每一份简历给出详细的修改完善建议,用心修改,深受好评!
6466

65-
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/xingqiu/%E7%AE%80%E5%8E%86%E4%BF%AE%E6%94%B92.jpg)
67+
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/xingqiu/image-20220725093504807.png)
6668

6769
### 一对一提问
6870

docs/java/collection/java-collection-questions-02.md

+60-4
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ head:
1818
1. **线程是否安全:** `HashMap` 是非线程安全的,`Hashtable` 是线程安全的,因为 `Hashtable` 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 `ConcurrentHashMap` 吧!);
1919
2. **效率:** 因为线程安全的问题,`HashMap` 要比 `Hashtable` 效率高一点。另外,`Hashtable` 基本被淘汰,不要在代码中使用它;
2020
3. **对 Null key 和 Null value 的支持:** `HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 `NullPointerException`
21-
4. **初始容量大小和每次扩充容量大小的不同 :** ① 创建时如果不指定容量初始值,`Hashtable` 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。`HashMap` 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 `HashMap` 会将其扩充为 2 的幂次方大小(`HashMap` 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 `HashMap` 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
22-
5. **底层数据结构:** JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间Hashtable 没有这样的机制。
21+
4. **初始容量大小和每次扩充容量大小的不同 :** ① 创建时如果不指定容量初始值,`Hashtable` 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。`HashMap` 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 `Hashtable` 会直接使用你给定的大小,而 `HashMap` 会将其扩充为 2 的幂次方大小(`HashMap` 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 `HashMap` 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
22+
5. **底层数据结构:** JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)以减少搜索时间(后文中我会结合源码对这一过程进行分析)。`Hashtable` 没有这样的机制。
2323

2424
**`HashMap` 中带有初始容量的构造函数:**
2525

@@ -184,9 +184,9 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
184184

185185
#### JDK1.8 之前
186186

187-
JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列****HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。**
187+
JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。HashMap 通过 key 的 `hashCode` 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
188188

189-
**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。**
189+
所谓扰动函数指的就是 HashMap 的 `hash` 方法。使用 `hash` 方法也就是扰动函数是为了防止一些实现比较差的 `hashCode()` 方法 换句话说使用扰动函数之后可以减少碰撞。
190190

191191
**JDK 1.8 HashMap 的 hash 方法源码:**
192192

@@ -229,6 +229,62 @@ static int hash(int h) {
229229

230230
> TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
231231
232+
我们来结合源码分析一下 `HashMap` 链表到红黑树的转换。
233+
234+
**1、 `putVal` 方法中执行链表转红黑树的判断逻辑。**
235+
236+
链表的长度大于 8 的时候,就执行 `treeifyBin` (转换红黑树)的逻辑。
237+
238+
```java
239+
// 遍历链表
240+
for (int binCount = 0; ; ++binCount) {
241+
// 遍历到链表最后一个节点
242+
if ((e = p.next) == null) {
243+
p.next = newNode(hash, key, value, null);
244+
// 如果链表元素个数大于等于TREEIFY_THRESHOLD(8)
245+
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
246+
// 红黑树转换(并不会直接转换成红黑树)
247+
treeifyBin(tab, hash);
248+
break;
249+
}
250+
if (e.hash == hash &&
251+
((k = e.key) == key || (key != null && key.equals(k))))
252+
break;
253+
p = e;
254+
}
255+
```
256+
257+
**2、`treeifyBin` 方法中判断是否真的转换为红黑树。**
258+
259+
```java
260+
final void treeifyBin(Node<K,V>[] tab, int hash) {
261+
int n, index; Node<K,V> e;
262+
// 判断当前数组的长度是否小于 64
263+
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
264+
// 如果当前数组的长度小于 64,那么会选择先进行数组扩容
265+
resize();
266+
else if ((e = tab[index = (n - 1) & hash]) != null) {
267+
// 否则才将列表转换为红黑树
268+
269+
TreeNode<K,V> hd = null, tl = null;
270+
do {
271+
TreeNode<K,V> p = replacementTreeNode(e, null);
272+
if (tl == null)
273+
hd = p;
274+
else {
275+
p.prev = tl;
276+
tl.next = p;
277+
}
278+
tl = p;
279+
} while ((e = e.next) != null);
280+
if ((tab[index] = hd) != null)
281+
hd.treeify(tab);
282+
}
283+
}
284+
```
285+
286+
将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树。
287+
232288
### HashMap 的长度为什么是 2 的幂次方
233289

234290
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ `(n - 1) & hash`”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。

0 commit comments

Comments
 (0)