HashMap
在平常开发中很是经常使用,它基于哈希表实现,以 key-value
形式存储。本文经过 JDK1.8
的源码,分析一下 HashMap
的内部结构和实现原理。html
在 JDK1.7
以前,HashMap
底层由数组 + 链表实现,也就是链表散列。当向 HashMap
中添加一个键值对时,首先计算 key
的 hash
值,以此肯定插入数组中的位置,但可能会碰撞冲突,将其转换为链表存储。java
而从 JDK1.8
开始,增长了红黑树,由数组 + 链表 + 红黑树实现,当链表长度超过 8
时,链表转换为红黑树以提升性能。它的存储方式以下:node
HashMap
的几个静态常量以下:算法
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 默认初始容量为 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量为 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子为 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 默认链表中元素大于 8 时转为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 扩容时,链表中元素小于这个值就会还原为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 数组的容量大于 64 时才容许被树形化
static final int MIN_TREEIFY_CAPACITY = 64;
···
}
复制代码
下面是 HashMap
中几个重要的变量:数组
transient Node<K,V>[] table; // 存储元素数组
transient Set<Map.Entry<K,V>> entrySet; // 缓存 entry 返回的 Set
transient int size; // 键值对个数
transient int modCount; // 内部结构修改次数
int threshold; // 临界值
final float loadFactor; // 负载因子
复制代码
Node<K,V>[] table缓存
Node<K,V>[] table
数组用来存储具体的元素,是 HashMap
底层数组和链表的组成元素。在第一次使用时初始化(默认初始化容量为 16
),并在必要的时候进行扩容。安全
通常来讲,因为素数致使冲突的几率较小,因此哈希表数组大小为素数。但 Java
的 HashMap
中采用很是规设计,数组的长度老是 2
的 n
次方,这样作能够在取模和扩容时作优化,同时也能减小碰撞冲突。app
Node
是 HashMap
的一个内部类,实现了 Map.Entry
接口,本质上就是一个映射(键值对)。它的实现以下:性能
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 用来定位数组索引位置
final K key; // 键
V value; // 值
Node<K,V> next; // 指向链表的下一个结点
Node(int hash, K key, V value, Node<K,V> next) { ··· }
public final K getKey() { ··· }
public final V getValue() { ··· }
public final String toString() { ··· }
// 重写了 hashCode 和 equals 方法
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) { ··· }
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;
}
}
复制代码
entrySet优化
entrySet
用于缓存 entrySet()
方法返回的 Set
。后面会详细分析。
size
size
是 HashMap
中键值对的数量。注意,键值对的数量 size
和哈希表数组的长度 capacity
不一样。
modCount
modCount
用于记录 HashMap
内部结构发生变化的次数,用于使用迭代器遍历集合时修改内部结构,而快速失败。须要注意的是,这里指的是结构发生变化,例如增长或删除一个键值对或者扩容,可是修改键值对的值不属于结构变化。
threshold 和 loadFactor
threshold
是 HashMap
能容纳的最大键值对个数,loadFactor
是负载因子,默认为 0.75
。有以下等式(capacity
是数组容量):
threshold = capacity * loadFactor;
复制代码
能够得出,在数组长度定义好以后,负载因子越大,所能容纳键值对越多。若是存储元素个数大于 threshold
,就要进行扩容,扩容后的容量是以前的两倍。
TreeNode
当链表长度超过 8
(阈值)时,将链表转换为红黑树存储,以提升查找的效率。下面是 TreeNode
的定义:
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;
}
}
······
}
复制代码
HashMap
主要提供了四种构造方法:
1). 构造一个默认初始容量 16
和默认加载因子 0.75
的空 HashMap
。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
复制代码
2). 构造一个指定的初始容量和默认加载因子 0.75
的空 HashMap
。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
复制代码
3). 构造一个指定的初始容量和加载因子的空 HashMap
。
public HashMap(int initialCapacity, float loadFactor) {
// check
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
复制代码
4). 使用给定的 map
构造一个新 HashMap
。
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
复制代码
HashMap
内部功能实现不少,这里主要从 hash
方法、put
方法、get
方法、resize
方法和 entrySet
方法进行分析。
HashMap
中,增删改查都须要用 hash
算法来计算元素在数组中的位置,因此 hash
算法是否均匀高效,对性能影响很大。看一下它的实现:
static final int hash(Object key) {
int h;
// 优化了高位运算算法
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// tab[i = (n - 1) & hash] 取模
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
···
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
···
}
复制代码
hash
算法计算对象的保存位置,分为三步:取 key
的 hashCode
值、高位运算、取模运算。
因为取模元素消耗较大,HashMap
中用了一个很巧妙的方法,利用的就是底层数组长度老是 2
的 n
次方。经过 hash & (table.length - 1)
就能够获得对象的保存位置,相较于对 length
取模效率更高。
JDK1.8
中优化了高位运算的算法,经过 hashCode
的高 16
位异或低 16
位实现。下面举例说明,n
为 table
的长度:
来看一下 HashMap
的 put
方法:
public V put(K key, V value) {
// 调用 hash 计算 key 的哈希值
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;
// 若是 table 为空或长度为 0,则调用 resize 进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 根据 key 的 hash 计算数组索引值,若是当前位置为 null,则直接建立新节点插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// table[i] 不为空
Node<K,V> e; K k;
// 若是 table[i] 的首元素和传入的 key 相等(hashCode 和 equals),则直接覆盖,这里允许 key 和 value 为 null
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断 table[i] 是否为 treeNode,即 table[i] 是否为红黑树,若是是则在树中插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 不然遍历链表
else {
for (int binCount = 0; ; ++binCount) {
// 若是 key 不存在
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 已经存在,则直接覆盖
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;
}
复制代码
下面是 put
方法的几个步骤::
table[]
为空或者长度为 0
,若是是则调用 resize()
进行扩容;hash & (table.length - 1)
计算插入的数组索引值,若是当前位置为 null
,则直接建立节点插入table[i]
的首个元素是否和 key
相等(hashCode
和 equals
),若是相等则直接覆盖 value
;table[i]
是否为 treeNode
,即 table[i]
是不是红黑树,若是是红黑树,则直接在树中插入键值对;key
不存在,则直接建立节点插入,并判断链表长度是否大于 8
,若是是红黑树则转为红黑树处理;若是遍历中发现 key
已经存在,则直接覆盖便可;HashMap
的 put
方法能够经过下图理解:
来看一下 HashMap
的 get
方法:
public V get(Object key) {
Node<K,V> e;
// 调用 getNode 方法,若是经过 key 获取的 Node 为 null,则返回 null;不然返回 node.value
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;
// 若是数组不为空,数组长度大于 0
// 经过 hash & (length - 1) 计算数组的索引值,而且对应的位置不为 null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 若是桶中第一个元素与 key 相等,则直接返回
if (first.hash == hash &&
((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;
}
复制代码
下面来分析一下 resize
方法的源码:
final Node<K,V>[] resize() {
// 保存原先的数组、容量、临界值
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 若是扩容前容量 > 0
if (oldCap > 0) {
// 若是数组大小已经达到最大 2^30,则修改阈值为最大值 2^31-1,之后也就不会再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 若是没有超过最大值,就扩充为原来的 2 倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
else if (oldThr > 0) // 若是扩容前容量 <= 0,旧临界值 > 0
// 将数组的新容量设置为 旧数组扩容的临界值
newCap = oldThr;
else { // 容量 <= 0,旧临界值 <= 0
// 不然设置为默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的临界值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 建立新的 table,容量为 newCap
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 遍历旧哈希表的每一个桶,将旧哈希表中的桶复制到新的哈希表中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 若是旧桶中只有一个 node
if (e.next == null)
// 则将 oldTab[j] 放入新哈希表中 e.hash & (newCap - 1) 的位置
newTab[e.hash & (newCap - 1)] = e;
// 若是旧桶中为红黑树,则转换处理
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
Node<K,V> loHead = null, loTail = null; // 将下标不变的节点组织成一条链表
Node<K,V> hiHead = null, hiTail = null; // 将下标增长 oldCapaciry 的节点组织成另外一条链表
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
// 原索引
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} else {
// 原索引 + oldCap
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到新数组中
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引 + oldCap 放到新数组中
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
复制代码
resize
方法在扩容时,因为每次数组的长度变为原先的 2
倍,因此元素要么在原位置,要么在“原始位置 + 原数组长度”的位置。经过计算 e.hash & oldCap
来判断是否须要移动。
看下图,n
为 table
的长度,图 (a)
为扩容前的 key1
和 key2
肯定索引位置的示例,图 (b)
为扩容后的 key1
和 key2
肯定索引位置的示例,其中 key1(hash1)
是 key1
对应的哈希与高位运算的结果:
元素在从新计算 hash
后,由于 n
变为 2
倍,那么 n - 1
的 mask
的范围(红色)在高位多 1bit
,所以新的 index
就会这样变化:
所以,在扩容时,只需看看原来的 hash
值新增的 bit
位是 1
仍是 0
,若是是 0
,索引不变,不然变成 "原索引 + oldCapacity
",能够看看下图 16
扩充为 32
的示意图:
HashMap
的一种遍历方式就是使用 entrySet
方法返回的迭代器进行遍历。先来看一下 entrySet
方法:
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
复制代码
能够看到,若是缓存 map
中键值对的 Set
不为 null
,则直接返回,不然会建立一个 EntrySet
对象。
EntrySet
类的 iterator
方法会返回一个 EntryIterator
迭代器对象,另外还有两个迭代器 KeyIterator
、ValueIterator
:
final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
final class KeyIterator extends HashIterator implements Iterator<K> {
public final K next() { return nextNode().key; }
}
final class ValueIterator extends HashIterator implements Iterator<V> {
public final V next() { return nextNode().value; }
}
复制代码
它们三个都继承自 HashIterator
,分别用于键遍历、值遍历、键值对遍历,它们都重写了 Iterator
的 next
方法,其中调用了 HashIterator
的 nextNode
方法。
而 HashIterator
是一个抽象类,实现了迭代器的大部分方法:
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() { ··· }
}
复制代码
能够看出 HashIterator
迭代器的默认构造器中,将 current
设置为 null
,而后循环在数组中查找不为 null
的桶, 让 next
指向第一个桶中的第一个节点 Node
。
在遍历时,next
方法会调用 nextNode()
方法,这个方法首先把 next
赋给 e
以稍后返回,并把 e
赋给 current
。而后判断 next
是否为空,若是不为空,返回 e
便可。
若是为空,就在数组中继续查找不为空的桶,找到后退出循环,最后返回 e
。这样就能都遍历出来了。
HashMap
的特色主要有:
HashMap
根据键的 hashCode
值来存储数据,大多数状况下能够直接定位它的值,于是访问速度很快。HashMap
不保证插入的顺序。HashMap
时,最好估算 map
的大小,初始化时给定一个大体的数值,避免进行频繁的扩容。threshold = capacity * loadFactor;
若是存储元素个数大于 threshold
,就要进行扩容,扩容后的容量是以前的两倍。0.75
是时间和空间之间的一个平衡,通常不建议修改。HashMap
中 key
和 value
容许为 null
,最多容许一条记录的键为 null
,容许多条记录的值为 null
。Collections
的 synchronizedMap
方法使 HashMap
具备线程安全的能力,或使用 ConcurrentHashMap
。