HashMap 在 JDK1.8 之前使用了 “数据 + 链表” 的结构,数组是 HashMap 的主体,链表主要是解决 哈希冲突而处在的。在 JDK 1.8 以后采用了 “数组+链表+红黑树” 的结构,当链表长度大于阈值,就可以将链表转化成红黑树。
如果所示这是 JDK1.7 基于 “数据+链表” 的 HashMap 结构。

在 JDK 1.8 中,当链表长度大于阈值时,就会将 “链表” 转化成 “红黑树”。

本文仅基于 JDK1.8 的源码进行分析。
首先看看 HashMap 的结构,如源码:
tatic class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
HashMap 的节点由四个元素组成,分别是 hash,key,value,以及指针(用于指向下一个节点)。
HashMap 在 put 元素时,
- 首先需要判断数组 table 的长度,如果首次判断为空,需要对数组长度进行初始化操作;
- 如果当前 node 不存在,将需要新建一个 node 节点来保存元素;
- 如果 node 存在,那么需要判断该节点元素是需要进行 “替换”,“红黑树插入”,还是 “链表插入”。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 初始化数组的长度。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// put 元素对于 HashMap 而言是新增加一个 Node 节点。
// (n - 1) & hash 来计算得到数组下标
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果 key 和 hash 是一模一样的,表明插入的节点已经存在,就可以将 oldNode 进行替换
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果是红黑树的结构,就可以对应的方式进行 Node 的插入。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 如果是链表结构,就以链表的方式进行 Node 的插入。
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果链表的长度大于 TREEIFY_THRESHOLD, 就需要将链表转化成红黑树
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;
p = e;
}
}
//如果插入的节点已经存在,就可以直接将节点的 value 进行替换操作。同时可以返回 oldValue 的值。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 当数组的长度超过阈值,那么需要重新分配数组的大小。threshold = 容量 * 负载因子
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
在阅读源码之前还需要了解
// HashMap 默认数组容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// HashMap 默认负载因子,则首次扩容在当 threshold > 12 时,即进行数组的扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// HashMap 数组容量的最大值
static final int MAXIMUM_CAPACITY = 1 << 30;
- 为什么每次容量扩容后得大小是 2 的幂次方
为了保证数据元素在存取时,尽量减少碰撞,保证数据元素尽可能的分配均匀。由于 HashMap 的元素的存储是根据 (n - 1) & hash 来计算下标。也就是先对数组长度取模运算,得到的余数是要存放元素位置的数组下标。“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length == hash&(length-1) 的前提是 length 是2的 n 次方”。
所以,如果链表长度小于 TREEIFY_THRESHOLD,那么存储方式类似于这样:
- 先判断 hash,如果不存在任何节点,则直接存放在数组上;
- 如果 hash 存在相等的情况,再判断 key 值是否存在;
- 如果 key 不存在,则生成一个新 Node,在前一个链表 Node 尾部添加;
- 如果 key 存在,则仅需对 Node 的值进行替换。

- 当链表长度大于默认值 8,那么链表将转化成红黑树,红黑树以实现 O(log n)时间复杂度内查找元素;

- HashMap 的 hash 实现
已经知道 hash 是进行取余来得到数组的下标位置的。由于容量采用的是 2 的幂次方,因此 hash集合可能会发生冲突。所以 hashCode 采用了 高 16 位 与 低 16 位进行异或计算,得到 hash。然后再对 HashCode 进行取余计算 下标 = (n-1)&hash。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 扩容过程
当数组的大小超过阈值(3/4 * capacity)时,此时 HashMap 的数组就需要扩容了。同时需要对所有节点重新计算哈希值。 此时新位置可能是原下标的位置,或者是(原下标 + 原容量)的位置。
403

被折叠的 条评论
为什么被折叠?



