调用 HashMap 的 put(K, V) 方法时,Node 变为 TreeNode 的条件
背景
HashMap.java 里的 put(K, V) 方法 调用了 putVal(int, K, V, boolean, boolean) 方法,后者有这样的代码 ⬇️
我把 TREEIFY_THRESHOLD 这个字段的内容复制到下方了 ⬇️
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
TREEIFY_THRESHOLD 的值为 ,从它的 javadoc 可以看出,当前 index 中的 entry 数量 时,list 可能转化为 tree(其实就是把 Node 转化为 TreeNode)。
我把本文开头红色框里的代码复制到下方了 ⬇️
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
这两行代码的意思是(在尚未添加新的 entry 时)如果当前这个 index 中的 entry 的数量 ,则调用 treeifyBin(tab, hash) 方法。
从名称来看,treeifyBin(...) 方法似乎会把指定 index 的 Node 转化为 TreeNode。但有特殊情况 ⬇️
当
table 字段的 length 小于 MIN_TREEIFY_CAPACITY () 时,resize() 方法会被调用,在此过程中,Node 不会 转化为 TreeNode。我们可以写点代码验证一下 Node 变为 TreeNode 的条件。
虽然可以通过打断点来查看 table 字段 的具体内容,但当 HashMap 中的 entry 数量较多时,查看各个 entry 的内容耗时耗力。
可以利用反射将 table 字段 的详细内容展示出来,这样可以直观地查看 table 字段 的变化。考虑到每个 entry 保存到 table 字段 的哪个 index 是由 entry 的 key 的 hashCode 来决定的,我们可以定义一个类 ,在 的构造函数中直接将 hashCode 传进来,这样就可以控制每个 entry 保存到哪个 index 了。
要点
调用 HashMap 的 put(K, V) 方法时,只有同时满足以下两个条件,Node 才会变为 TreeNode
- (在尚未添加新的
entry时)当前这个index中的entry的数量
正文
用于查看 HashMap 详情的代码
请将以下代码保存为 HashMapInspector.java ⬇️
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class HashMapInspector {
private static final Class<?> hashMapClass = HashMap.class;
private <K, V> void showBasicInfo(HashMap<K, V> map, Map.Entry<K, V>[] table) throws Exception {
Field thresholdField = hashMapClass.getDeclaredField("threshold");
thresholdField.setAccessible(true);
System.out.println("Basic Info ⬇️");
System.out.printf("* `size` is: `%s`%n", map.size());
System.out.printf("* `threshold` is: `%s`%n", thresholdField.get(map));
if (table == null) {
System.out.printf("* `table` field is `null`%n");
} else {
System.out.printf("* `table.length` is: `%s`%n", table.length);
}
System.out.println();
}
public <K, V> void showHashMapDetails(HashMap<K, V> map) throws Exception {
Field tableField = hashMapClass.getDeclaredField("table");
tableField.setAccessible(true);
@SuppressWarnings("unchecked")
Map.Entry<K, V>[] table = (Map.Entry<K, V>[]) tableField.get(map);
showBasicInfo(map, table);
if (table == null) {
return;
}
Class<?> nodeClass = Class.forName("java.util.HashMap$Node");
Field nextField = nodeClass.getDeclaredField("next");
nextField.setAccessible(true);
System.out.println("Details for each index in `table` field ⬇️");
System.out.println("| Index | Entry's class | Node count | Details |");
System.out.println("| --- | --- | --- | --- |");
int currentIndex = 0;
while (currentIndex < table.length) {
Map.Entry<K, V> entry = table[currentIndex];
if (entry == null) {
int rightBound = currentIndex;
while (rightBound + 1 < table.length && table[rightBound + 1] == null) {
rightBound++;
}
if (rightBound > currentIndex) {
System.out.printf("| `%s` to `%s` | (entry is null) | `0` | `null` |%n", currentIndex, rightBound);
} else {
System.out.printf("| `%s` | (entry is null) | `0` | `null` |%n", currentIndex);
}
currentIndex = rightBound + 1;
} else {
StringBuilder details = new StringBuilder();
details.append(entry);
int nodeCnt = 1;
while (true) {
@SuppressWarnings("unchecked")
Map.Entry<K, V> next = (Map.Entry<K, V>) nextField.get(entry);
if (next == null) {
break;
}
entry = next;
details.append(" -> ");
details.append(next);
nodeCnt++;
}
String classDesc = String.format("`%s`", entry.getClass().getCanonicalName());
String row = String.format("| `%s` | %s | `%s` | `%s` |", currentIndex, classDesc, nodeCnt, details);
System.out.println(row);
currentIndex++;
}
}
System.out.println();
}
}
class C {
private final int v;
private final int h;
C(int val, int hash) {
this.v = val;
this.h = hash;
}
@Override
public int hashCode() {
return h;
}
@Override
public String toString() {
return "C{v=" + v + ", h=" + h + '}';
}
}
HashMapInspector 和 C 的类图如下 ⬇️ (所有泛型和方法签名中的异常均省略)
classDiagram
class HashMapInspector {
-Class hashMapClass$
-showBasicInfo(HashMap, Map.Entry[]) void
+showHashMapDetails(HashMap) void
}
class C {
-int v
-int h
~C(int, int)
+hashCode() int
+toString() String
}
用如下的命令可以编译 HashMapInspector.java ⬇️
javac HashMapInspector.java
编译之后会生成以下两个 class 文件
HashMapInspector.classC.class
有了这两个 class 文件后,我们可以方便地查看 HashMap 中 table 字段的变化。
情形 1: 将 11 个 entry 都保存在 HashMap 里 index=0 的位置
请将以下代码保存为 Case1.java ⬇️
import java.util.HashMap;
public class Case1 {
public static void main(String[] args) throws Exception {
HashMap<C, Integer> map = new HashMap<>();
HashMapInspector inspector = new HashMapInspector();
for (int i = 1; i <= 8; i++) {
map.put(new C(i, 0), map.size());
}
inspector.showHashMapDetails(map);
map.put(new C(9, 0), map.size());
inspector.showHashMapDetails(map);
map.put(new C(10, 0), map.size());
inspector.showHashMapDetails(map);
map.put(new C(11, 0), map.size());
inspector.showHashMapDetails(map);
}
}
Case1.java 的 main(...) 方法里先用默认构造函数创建一个 HashMap ,之后有以下 步 ⬇️
- 向 中添加 个
key-valuepair并通过HashMapInspector展示table字段的详细内容 - 向 中添加第 个
key-valuepair并通过HashMapInspector展示table字段的详细内容 - 向 中添加第 个
key-valuepair并通过HashMapInspector展示table字段的详细内容 - 向 中添加第 个
key-valuepair并通过HashMapInspector展示table字段的详细内容
用如下的命令可以编译 Case1.java 并运行其中的 main(...) 方法 ⬇️
javac -cp . Case1.java
java --add-opens java.base/java.util=ALL-UNNAMED Case1
第 1 次展示 table 字段的详细内容: 当 里有 8 个 entry 时
Basic Info ⬇️
sizeis:8thresholdis:12table.lengthis:16
Details for each index in table field ⬇️
| Index | Entry's class | Node count | Details |
|---|---|---|---|
0 | java.util.HashMap.Node | 8 | C{v=1, h=0}=0 -> C{v=2, h=0}=1 -> C{v=3, h=0}=2 -> C{v=4, h=0}=3 -> C{v=5, h=0}=4 -> C{v=6, h=0}=5 -> C{v=7, h=0}=6 -> C{v=8, h=0}=7 |
1 to 15 | (entry is null) | 0 | null |
当 里有 个 entry 时,由于这些 entry 的 key 的 hashCode 都是 ,所以这 个 entry 都会保存在 的位置。
第 2 次展示 table 字段的详细内容: 当 里有 9 个 entry 时
Basic Info ⬇️
sizeis:9thresholdis:24table.lengthis:32
Details for each index in table field ⬇️
| Index | Entry's class | Node count | Details |
|---|---|---|---|
0 | java.util.HashMap.Node | 9 | C{v=1, h=0}=0 -> C{v=2, h=0}=1 -> C{v=3, h=0}=2 -> C{v=4, h=0}=3 -> C{v=5, h=0}=4 -> C{v=6, h=0}=5 -> C{v=7, h=0}=6 -> C{v=8, h=0}=7 -> C{v=9, h=0}=8 |
1 to 31 | (entry is null) | 0 | null |
注意,Case1.java 里的 main(...) 方法添加第 9 个 key-value pair 时, 的 Node 并没有变为 TreeNode,因为 的条件还不成立。
第 3 次展示 table 字段的详细内容: 当 里有 10 个 entry 时
Basic Info ⬇️
sizeis:10thresholdis:48table.lengthis:64
Details for each index in table field ⬇️
| Index | Entry's class | Node count | Details |
|---|---|---|---|
0 | java.util.HashMap.Node | 10 | C{v=1, h=0}=0 -> C{v=2, h=0}=1 -> C{v=3, h=0}=2 -> C{v=4, h=0}=3 -> C{v=5, h=0}=4 -> C{v=6, h=0}=5 -> C{v=7, h=0}=6 -> C{v=8, h=0}=7 -> C{v=9, h=0}=8 -> C{v=10, h=0}=9 |
1 to 63 | (entry is null) | 0 | null |
注意,Case1.java 里的 main(...) 方法添加第 10 个 key-value pair 时, 的 Node 并没有变为 TreeNode,因为 的条件还不成立。
第 4 次展示 table 字段的详细内容: 当 里有 11 个 entry 时
Basic Info ⬇️
sizeis:11thresholdis:48table.lengthis:64
Details for each index in table field ⬇️
| Index | Entry's class | Node count | Details |
|---|---|---|---|
0 | java.util.HashMap.TreeNode | 11 | C{v=3, h=0}=2 -> C{v=1, h=0}=0 -> C{v=2, h=0}=1 -> C{v=4, h=0}=3 -> C{v=5, h=0}=4 -> C{v=6, h=0}=5 -> C{v=7, h=0}=6 -> C{v=8, h=0}=7 -> C{v=9, h=0}=8 -> C{v=10, h=0}=9 -> C{v=11, h=0}=10 |
1 to 63 | (entry is null) | 0 | null |
注意,Case1.java 里的 main(...) 方法添加第 11 个 key-value pair 时, 的 Node 并变为 TreeNode 了,因为 的条件成立。
小结
Case1.java 的 main(...) 方法,在创建 HashMap 后,会调用 put(K, V) 方法将新的 entry 保存至 table 字段中 的位置。下表列举了当 entry 的数量是 时, 的值和 处的对象的类型 ⬇️
中 entry 的数量 | 处的对象的精确类型 | |
|---|---|---|
java.util.HashMap.Node | ||
java.util.HashMap.Node | ||
java.util.HashMap.Node | ||
java.util.HashMap.TreeNode |
情形 2: 用 new HashMap(64) 创建 map 后,将 9 个 entry 都保存在 index=0 的位置
请将以下代码保存为 Case2.java ⬇️
import java.util.HashMap;
public class Case2 {
public static void main(String[] args) throws Exception {
HashMap<C, Integer> map = new HashMap<>(64);
HashMapInspector inspector = new HashMapInspector();
for (int i = 1; i <= 8; i++) {
map.put(new C(i, 0), map.size());
}
inspector.showHashMapDetails(map);
map.put(new C(9, 0), map.size());
inspector.showHashMapDetails(map);
}
}
Case2.java 的 main(...) 方法里先用 new HashMap(64) 的方式创建一个 HashMap ,之后有以下 步 ⬇️
- 向 中添加 个
key-valuepair并通过HashMapInspector展示table字段的详细内容 - 向 中添加第 个
key-valuepair并通过HashMapInspector展示table字段的详细内容
用如下的命令可以编译 Case2.java 并运行其中的 main(...) 方法 ⬇️
javac -cp . Case2.java
java --add-opens java.base/java.util=ALL-UNNAMED Case2
第 1 次展示 table 字段的详细内容: 当 里有 8 个 entry 时
Basic Info ⬇️
sizeis:8thresholdis:48table.lengthis:64
Details for each index in table field ⬇️
| Index | Entry's class | Node count | Details |
|---|---|---|---|
0 | java.util.HashMap.Node | 8 | C{v=1, h=0}=0 -> C{v=2, h=0}=1 -> C{v=3, h=0}=2 -> C{v=4, h=0}=3 -> C{v=5, h=0}=4 -> C{v=6, h=0}=5 -> C{v=7, h=0}=6 -> C{v=8, h=0}=7 |
1 to 63 | (entry is null) | 0 | null |
第 2 次展示 table 字段的详细内容: 当 里有 9 个 entry 时
Basic Info ⬇️
sizeis:9thresholdis:48table.lengthis:64
Details for each index in table field ⬇️
| Index | Entry's class | Node count | Details |
|---|---|---|---|
0 | java.util.HashMap.TreeNode | 9 | C{v=3, h=0}=2 -> C{v=1, h=0}=0 -> C{v=2, h=0}=1 -> C{v=4, h=0}=3 -> C{v=5, h=0}=4 -> C{v=6, h=0}=5 -> C{v=7, h=0}=6 -> C{v=8, h=0}=7 -> C{v=9, h=0}=8 |
1 to 63 | (entry is null) | 0 | null |
可见当 的位置的 entry 的数量超过 个时,Node 的确变成了 TreeNode。
小结
Case2.java 的 main(...) 方法,用 new HashMap(64) 的方式创建一个 HashMap 后,会调用 put(K, V) 方法将新的 entry 保存至 table 字段中 的位置。下表列举了当 entry 的数量是 时, 的值和 处的对象的类型 ⬇️
中 entry 的数量 | 处的对象的精确类型 | |
|---|---|---|
java.util.HashMap.Node | ||
java.util.HashMap.TreeNode |