在 jdk1.8 以前的 HashMap 的实现是数组+链表,即使哈希函数取得再好,也很
难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放在同一个桶
中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,
假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
针对这种情况,JDK1.8 引入了红黑树(查找时间复杂度为 O(logn))来优化这
个问题。
下面具体看 put 方法的源码。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1 如果 table 为空或者长度为 0,即没有元素,则使用 resize()方法扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2 计算插入存储的数组索引 i,如果数组当前位置为空,则不存在 Hash 冲突,可以直
接插入元素。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 3 插入新元素时,如果发生 Hash 冲突,则依次往下判断。
else {
Node<K,V> e; K k;
// 3.1 判断 table[i]的元素的 key 是否与需要插入的 key 相同,若相同则直接用
新的 value 覆盖掉旧的 value,判断元素相等原则是 equals(),所以 key 对象需要重写该方
法。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 3.2 判断需要插入的数据结构是红黑树还是链表,如果是红黑树,则直接在树中
通过 putTreeVal()方法插入/更新键值对。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 3.3 如果是链表,则在链表中插入/更新键值对
else {
// 3.3.1 遍历 table[i],判断 key 是否已经存在,采用 equals 对比当前遍
历结点的 key 与需要插入数据的 key,如果存在相同,则直接覆盖。
// 3.3.2 遍历完毕后如果没有发现相同的 key,直接在链表尾部插入新的节点
元素。
// 插入完成后判断链表长度是否>TREEIFY_THRESHOLD(8),若是,则把链
表转换为红黑树。
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;