1. HashMap 类定义:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
其中哈希表(Hash table)table
的定义:
static final int MAXIMUM_CAPACITY = 1 << 30;
由注释知 table的长度是2的N次方,每次扩容变为原来的两倍。所以这是 table.length
数组的最大长度。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
table 默认大小为16,可通过构造函数改变
static final int TREEIFY_THRESHOLD = 8;
链表转换成红黑树的阈值
static final int UNTREEIFY_THRESHOLD = 6;
红黑树转换回链表的阈值
static final int MIN_TREEIFY_CAPACITY = 64;
当table的长度 < 64 时,就算某个桶中的链表超过8,也不转换为红黑树,而是进行 resize()
操作。
Node类的实例变量:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 记录K的 Hash值
final K key; // K 不可变
V value; // V 可变
Node<K,V> next; // 采用拉链法处理冲突,指向下一个Node对象
可以看出使用拉链法处理Hash冲突。
图片来源网络,这里的Entry应该改为Node。
2. put 操作
其中的 hash() 函数当 key 为 null 时返回 0 ,即 key 为 null 的键值对直接存储在 table[0] 桶中。对于key不是null的键值对,取 ( h = key.hashCode()) ^ (h >>> 16)
为Hash值。 相当于前16位不变,后16位与前16位做异或,以此为hash值。
>>>
是无符号右移,忽略符号位,空位都以0补齐。
^
是按位异或。
关键的putVal()函数
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) // 是否已经初始化table
n = (tab = resize()).length; // 初始化table.
if ((p = tab[i = (n - 1) & hash]) == null) // 计算hash值对应的table 桶
// (n - 1) & hash 等价于 (hash % n)
tab[i] = newNode(hash, key, value, null);
// 如果此桶还没有元素,则创建新Node节点并直接指向它。
else { // 如果此桶已有元素。
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果此位置指向的第一个 Node 的 key 与 put 的 key 相同
e = p;
else if (p instanceof TreeNode) // TreeNode 是 Node 的子类
// 如果已经超过阈值,由链表转化成了红黑树。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 向树中插入新节点
else { // 还是链表,并且第一个Node的key和put的key不匹配
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);
// 遍历链表到结尾,没有发现相同的key,要创建新节点时,
// 如果发现链表长度超过阈值,则转化为红黑树。
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 对比时,先对比hash值,如果两对象相同,hash值肯定相同,不相同则直接下一个。
// 如果hash相同,再对比key指针是否指向同一个对象,如果key不相同,则使用 equals()
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
// 如果允许更新或原来的value为null,默认允许改变value
e.value = value; // 更新value值,并非创建新节点
afterNodeAccess(e); // 实现为空
return oldValue;
}
}
++modCount; // 记录结构修改次数,实现 fail-fast 迭代器。
if (++size > threshold) // 每次插入新节点就增加 size,超过 threshold 则 resize()
resize();
afterNodeInsertion(evict); // 实现为空
return null;
}
继承自Node的TreeNode类型,充当红黑树的节点。
treeifyBin()
是把链表转换为红黑树的函数,可以看出,这里只转换长度超出阈值的那个链表,而不是把table上的都转换成红黑树。
- 其中的 resize() 函数用来
Initializes or doubles table size.
,可见每次扩容长度变成原来的两倍。 - (n - 1) & hash 等价于 (hash % n) ,位运算效率更快。 其中 n 是 table 的长度,是2的x次方,所以此位运算成立。
- key 为 null 的 hash 值规定为 0。
- TreeNode 是 Node 的子类,当此链表长度超过
static final int TREEIFY_THRESHOLD
时,将Node类型链表转换为节点类型为TreeNode的红黑树。反之,如果在resize()
时发现新哈希位置上的红黑树节点数量少于static final int UNTREEIFY_THRESHOLD
时,将红黑树转换为链表。 - 当table的长度 <
MIN_TREEIFY_CAPACITY = 64
时,就算某个桶中的链表超过8,也不转换为红黑树,而是进行resize()
扩容操作。 - JDK 1.7 中采用头插法并且不能转化为红黑树。JDK 1.8 中采用尾插法并且长度超过阈值时转换为红黑树。
- HashMap 的迭代器是
fail-fast
迭代器。
3. 扩容
设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。
为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。
如果table.length
已经为最大值MAXIMUM_CAPACITY = 1 << 30
,则不能再扩容,则把 threshold 改为 Integer.MAX_VALUE
,不再考虑装载因子,如果持续插入,当 size
超过 Integer.MAX_VALUE
时会变为负值。
和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。
capacity()
: table 的长度,默认为 16,table != null
时等于table.length
。需要注意的是 capacity 必须保证为 2 的 n 次方。transient int size
: map 中 key-value 对的个数。int threshold
: size 的临界值,size 必须小于 threshold,如果大于等于,就必须进行扩容操作。final float loadFactor
: 装载因子,table 能够使用的比例,threshold = capacity * loadFactor
。DEFAULT_LOAD_FACTOR
默认为0.75f
。- 可以通过构造函数自定义 初始的table长度 和 loadFactor。
关键的 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;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
// 如果table不能再变长,则改threshold为int最大整数,不再考虑装载因子。
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
// table还能变大,则 capacity 和 threshold 都乘2
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
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;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 创建新的 table 引用数组
table = newTab;
if (oldTab != null) {
// 转移原 table 上的 Node 到新的 table
for (int j = 0; j < oldCap; ++j) {
// 遍历原table数组
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果此桶只有一个Node节点,直接重新插入到新table
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 如果已经是红黑树
else { // preserve order
// 非树类型,桶内节点不止一个,节点在新桶中会保持顺序
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
// 看旧capacity的位置是否为0
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
// 如果旧capacity的位置为1
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
// 还在新table原位置
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
// 在新table [原结果 + oldCap] 这个位置
}
}
}
}
}
return newTab;
}
在进行扩容时,需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32,Key在新数组上的位置只有两种可能:
- 它的哈希值如果在第 5 位上为 0 (即旧capacity的位置,网上有博客写是第六位,即新capacity的位置,这是不对的),那么取模得到的结果和之前一样,即还在原位置;
- 如果为 1,那么得到的结果为原来的结果 +16,即在 [原位置 + oldCap] 这个位置。
table 长度是2的N次方的好处
- 当table扩容,计算Node新位置时,可以用位运算
e.hash & oldCap
加速,值为0则还在[原位置],值不为0则在[原位置 + oldCap]。 - 计算hash值对应的桶时,可用位运算
(n - 1) & hash
加速(hash % n)
。(因为n是2的N次方才成立)
其他一些细节
- 在链表中对比时,先对比hash值(如果两对象相同,hash值肯定相同),不相同则直接下一个。如果hash相同,再对比key指针是否指向同一个对象,如果key不相同,最后使用 equals() 判断。
- JDK 1.8 时,插入key-values时如果发现链表长度超过阈值(8),则转化为红黑树。
- 当执行
resize()
操作时,如果某桶中是红黑树会执行split()
把红黑树分割成两个树,如果某个树的节点个数小于UNTREEIFY_THRESHOLD
,则会执行untreeify()
把该树转换回链表。
当构造HashMap时,传入的 initialCapacity 不是2的N次方怎么处理
HashMap 的构造函数:
public HashMap(int initialCapacity, float loadFactor) {
... ...
this.threshold = tableSizeFor(initialCapacity);
}
其中 tableSizeFor()
函数返回大于initialCapacity的最大二进制数。
比如 cap 为 0010 0100
,则为 n 为 0011 1111
,最后返回 n + 1,即为 0100 0000
。
static final int tableSizeFor(int cap) {
int n = cap - 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;
}
使用 modCount 实现 fail-fast
迭代器
和其他容器类一样,使用modCount 记录结构被修改的次数,以此实现 fail-fast
(快速失败)迭代器。
4. 序列化
这里不序列化 table 表、modCount 等参数。
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject(); // ignore transient variable
s.writeInt(buckets); // the number of buckets
s.writeInt(size); // the number of key-value mappings
internalWriteEntries(s); // all key-value mappings
}
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}
5. 与 JDK 1.7 的比较
- 1.7 头插法可能命中率会更高,类似LRU。但线程不安全,在多个线程执行
resize()
扩容时,容易形成环。1.8 采用尾插法,则不会。 - JDK 1.7 中采用头插法并且不能转化为红黑树。JDK 1.8 中采用尾插法并且长度超过阈值时转换为红黑树。
- rehash 时 1.7 直接用位运算
(n - 1) & hash
加速(hash % n)
并且头插,在新桶中不保持原有顺序。 1.8 看 (e.hash & oldCap) 是否为 0,尾插,在新桶中会保持顺序 - hash() 函数的实现不同,1.7有随机生成的 hashSeed ,1.8 的hash() 函数更简单,没有 hashSeed。
为什么HashMap线程不安全
- 因为所有函数没有加同步,
size++
操作就不安全。 - 1.7 时在多个线程执行
resize() -> transfer()
扩容时,容易形成环,如下所述。
// JDK 1.7 !!
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
// 两个线程同时执行此函数,可能会形成环
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; // 可以看出是头插法
newTable[i] = e;
e = next;
}
}
}
table[i] 是不变的,所以线程二执行时,e == A
。
6. 与 HashTable 的比较
HashTable
使用synchronized
来进行同步(线程安全)。JDK 5
提供了ConcurrentHashMap
,它是HashTable
的替代,使用分段锁实现线程安全,比HashTable
的性能更好。HashMap
可以插入key为 null 的 Node,HashTable
和ConcurrentHashMap
插入 key 为 null 时会抛空指针异常。HashMap
和HashTable
的迭代器均为fail-fast
迭代器,ConcurrentHashMap
没有fail-fast
迭代器。HashMap
不能保证随着时间的推移Map
中的元素次序是不变的。Hashtable
以key.hashCode() & 0x7FFFFFFF
为Hash方式,HashMap
以( h = key.hashCode()) ^ (h >>> 16)
为Hash方式。