概述
HashMap 是一个关联数组、哈希表、红黑树的集合。是线程不安全的,允许key为null,value为null。遍历时无序。
底层数据结构是数组称之为哈希表,每个位置里面放的是链表,链表中的每个节点,就是HashMap中的每个元素。
如果链表里面的元素大于等于8个,就把链表转化成红黑树,提高查询效率。
HashMap 的结构大致如下:
下图是HashMap 继承和实现的接口:
基本数据结构
桶数组
一个数组,每个位置后面是一个链表。
transient Node<K,V>[] table;
链表节点Node
static
class Node<K,V> implements Map.Entry<K,V> {
final int hash; // hash值
final K key;
V value;
Node<K,V> next; // 链表下一个节点
// 构造函数
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
// 节点的 hashCode 为 key 和 value 的 hashCode 异或得到。
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
// 设置新的值, 返回旧的值
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 判断是不是一个节点。
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
红黑树
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父亲节点
TreeNode<K,V> left; // 左儿子
TreeNode<K,V> right; // 右儿子
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red; // 颜色
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// 返回根节点
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
基本元素
看源码之前要知道一些常量的意思,大概了解一下,有个印象就可以了。
负载因子
总容量 * 负载因子 = 阈值
如果map里面的元素个数大于阈值, 就需要扩容了。
如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(最新版本使用了红黑树后会改进很多),扩容之后,将原来链表数组的每一个链表分成两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率
// 默认的初始容量为 16. 容量的大小一定是 2 的若干次方。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 容量的最大值不超过 1 << 30.
static final int MAXIMUM_CAPACITY = 1 << 30;
// 负载因子为 0.75.
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 树化的阈值。单个链表里面的元素个数。 链表转换成红黑树的判断条件。
static final int TREEIFY_THRESHOLD = 8;
// 树退化的阈值。单个链表里面的元素个数。 红黑树退化的判断条件
static final int UNTREEIFY_THRESHOLD = 6;
// 最小的树化总容量。这里指的是map里面的元素总个数。
static final int MIN_TREEIFY_CAPACITY = 64;
// ----------------------------------------------
// hash表,每个位置里面有若干个元素。
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
// map 里面的元素个数
transient int size;
// 修改次数
transient int modCount;
// 扩容的阈值, 超过阈值要扩容。(capacity * load factor)
int threshold;
// hash 表的负载因子
final float loadFactor;
构造函数
构造函数一共有四种,具体可以看下面代码注释。
// 给定 初始容量, 负载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
// 给定初始容量
public HashMap(int initialCapacity) {
// 调用上面的构造函数,给定 容量和负载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 默认情况
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 从已有的 map 创建一个新的 map。
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
构造函数会调用下面的代码块。
// 返回大于等于 cap 的最小 2 的若干次方。
static final int tableSizeFor(int cap) {
int n = cap - 1;
// 就是把二进制最高位的1的后面位置全部变成1.
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
// 和最大容量比较一下, 不能超过最大容量。
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
// 计算阈值的大小。
float ft = ((float)s / loadFactor) + 1.0F;
// 和最大容量比较, 如果超过最大容量就变成最大容量。
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
// 重新计算阈值大小, 变成 2 的若干次方。
threshold = tableSizeFor(t);
}
else if (s > threshold)
// 如果 已有的集合元素个数大于阈值要扩容。
resize();
// 遍历集合, 一个一个的加入当前集合。
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
HashMap 的存取与扩容
扩容
HashMap 默认的容量为16,扩容的时候调整为原来大小的两倍。所以容量一定是2的若干次方。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 得到 map 的大小。
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 当前阈值
int oldThr = threshold;
int newCap, newThr = 0;
// 当前的容量大于 0
if (oldCap > 0) {
// 当前容量达到上限了
if (oldCap >= MAXIMUM_CAPACITY) {
// 阈值设为2^31-1
threshold = Integer.MAX_VALUE;
// 返回当前的hash表, 不再扩容
return oldTab;
}
// 新容量扩大到旧容量的两倍, 新容量小于最大容量且旧容量大于等于16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 阈值扩大成原来的两倍
} // 如果当前表是空的,但是有阈值。代表是初始化时指定了容量、阈值的情况
else if (oldThr > 0) // 如果有旧阈值, 新容量等于旧阈值,旧阈值是 2 的若干次方
newCap = oldThr;
else { // 默认情况, 没有容量和阈值, 那么就设置默认容量和阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) { // 如果新的阈值为0, 要重新计算阈值。
float ft = (float)newCap * loadFactor;
// 防止阈值越界
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 更新阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 根据新的容量, 构建新的hash表
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 更新引用
table = newTab;
// 如果旧表不为空
if (oldTab != null) {
// 循环遍历旧hash表的每一个位置。
for (int j = 0; j < oldCap; ++j) {
// 临时节点 e
Node<K,V> e;
// 临时节点 引用当前位置。
if ((e = oldTab[j]) != null) {
// 旧hash表的位置置为null, 便于GC
oldTab[j] = null;
// 如果当前位置只有一个元素, 插入到新表里面去,
// e.hash & (newCap - 1) 这个是计算当前节点在哪个位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果是红黑树, 按照红黑树的方法把当前位置的节点分开
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 低位链表的头和尾,0 ~ oldCap - 1
Node<K,V> loHead = null, loTail = null;
// 高位链表的头和尾 oldCap ~ newCap - 1
Node<K,V> hiHead = null, hiTail = null;
// 临时变量
Node<K,V> next;
do {
next = e.next;
// 判断二进制的最高位是0还是1, 如果是 0,节点放在低位链表, 否则高位链表。
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} // 二进制的最高位是 1,节点放在高位链表
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 低位链表放在 j 的位置上,
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 高位链表放在 j + oldCap 的位置上。
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
-
如果当前容量大于0,当前容量达到上限,直接返回,不扩容。没达到上限扩容成原来的两倍。
-
如果当前容量为0, 设置容量和阈值为默认值。
-
把旧表里面的元素分类,计算每个key的hash值,判断key是在(原位置)还是(原位置+旧容量)
putVal
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) {
// tab 存放当前的hash表, p 是临时变量。
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果hash表是空的, 首先要扩容。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算在hash表中的位置, 如果位置是空的, 就没有hash冲突, 直接新建一个节点就可以了。
// 找 hash 表中的位置, 用的是位运算, 加快运算速度。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// e 临时变量,如果不为null, 代表key 一样, 要覆盖value。
Node<K,V> e; K k;
// hash 一样, 找到位置, key 也一样, 要覆盖。 e 记录要覆盖的位置。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 红黑树的情况。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 遍历链表,插入value
for (int binCount = 0; ; ++binCount) {
// 如果到了链表尾部, 把 value 添加进去就好了。
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果添加之后的节点个数 >= 8 转化成红黑树。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 在遍历的过程中, key 一样,要覆盖, e 是要覆盖的位置, 直接break。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 有要覆盖的位置。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果元素个数大于阈值, 要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
①.判断键值对数组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,如果超过,进行扩容。
getVal
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// hash表里面有内容, 然后找到 key 在hash 表里面的位置。
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 总是判断第一个节点是不是要找的节点。如果是就返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 如果是红黑树, 去红黑树里面找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 在链表里面找
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
-
map里面有内容,根据key 的hash值,找到key所在的index。
-
找到index,判断第一个节点是不是要找的节点,如果是就返回。
-
判断下面节点是不是红黑树,如果是就去红黑树里面找。
-
不是红黑树,就遍历链表找。
-
找不到就返回null
其他
HashMap 的长度为什么是2的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。在扩容,添加的过程中,也使用位运算加快运算速度。比如计算key的下标位置,扩容之后判断每个节点是在原下标位置还是更新下标位置。