概念
HashMap是基于Hash表的Map实现,提供所有可选的键值映射操作。底层实现是数组单链表实现,数组中的每个元素是一个链表节点,通过链表来处理hash冲突。
源码解析
源码解析只涉及到一些核心功能实现分析,不会对HashMap的所有源码进行展开。
字段属性
// 默认HashMap容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大HashMap容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 结点数组
transient Entry<K,V>[] table;
// 数组大小
transient int size;
// 阈值。等于size * loadFactor, 当容量超过阈值会进行扩容操作;
int threshold;
// 加载因子, 衡量HashMap满的指标, 默认0.75。
final float loadFactor;
单链表节点结构
// 单链表结点结构, 实现Map.Entry接口来操作Map键值对
static class Entry<K, V> implements Map.Entry<K, V> {
final K key;
V value;
Entry<K, V> next;
int hash;//对输入对象进行hash函数生成,相同的对象只会生成相同的hash值, 不同的对象可能生成相同的hash值(碰撞或者冲突)。
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
构造函数
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);
// 寻找一个大于等于initialCapacity的2次幂的数
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
// 初始化参数和节点数组
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
方法分析
put
// 添加键值对
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
// 计算key的hash值
int hash = hash(key);
// 通过hash值计算出table下标
int i = indexFor(hash, table.length);
for(Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
// 结点已存在值,覆盖并返回旧值
if (e.hash == hash && ((k = e.key) == key) || e.value == value) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 添加键值对到结点数组中
addEntry(hash, key, value, i);
return null;
}
添加键值对步骤:
1. 计算key的hash值。
2. 通过hash值查找节点数组下标。
3. 如果所在下标节点链表存在value覆盖旧值。
4. 添加键值对到节点数组中。
// hash函数
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
// 因为HashMap是通过hash值与HashMap的长度-1进行位运算取得其桶的下标来取代通过hash值对HashMap的长度进行取模运算
// 但是通过位运算进行定位桶下标只利用了hash值的低位值,这样很容易产生hash冲突,因此通过位移将高位和低位进行混合扰动来
// 规避只利用hash值的低位弊端来减少hash冲突的产生。
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// 通过hash值寻找桶下标
static int indexFor(int h, int length) {
// 因为位运算比取模运算更加高效,这里通过位运算来取代取模运算,但是这里有个限制那就是其length长度必须是2的幂次数。
// 例如:2^n表示2的n次方,也就是说,一个数对2^n取模 == 一个数和(2^n - 1)做按位与运算 。
// 假设n为3,则2^3 = 8,表示成2进制就是1000。2^3 -1 = 7 ,即0111。此时X & (2^3 - 1) 就相当于取X的2进制的最后三位数。
// 从2进制角度来看,X / 8相当于 X >> 3,即把X右移3位,此时得到了X / 8的商,而被移掉的部分(后三位),则是X % 8,也就是余数。[全网把Map中的hash()分析的最透彻的文章,别无二家](https://coolshell.cn/articles/9606.html%5D%28https://coolshell.cn/articles/9606.html)
return h & (length-1);
}
// 构建键值对节点添加到数组链表中
void addEntry(int hash, K key, V value, int bucketIndex) {
// 数组大小超过threshold, 哈希表进行rehash操作(原来数组大小扩展成两倍,将原有数组数据复制到新建数组)。
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
// 重新计算hash值和数组下标
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
// 插入键值对到数组链表中
createEntry(hash, key, value, bucketIndex);
}
// 新建链表节点,插入到单链表头部。
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
// 新建Entry结点,插入到单链表头部。
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
size++;
}
resize
// 当HashMap容量超过阈值会进行rehash扩容操作
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 如果数组超过最大容量不扩容直接返回
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 新建数组,大小为原来数组大小的两倍
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
// 将老的数组数据复制到新的数组中
transfer(newTable, rehash);
// 重新设置table和threshold
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
// 将旧数组各个元素重新hash到新数组中
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);
}
// 计算key值在新数组的下标
int i = indexFor(e.hash, newCapacity);
// 将老的数组下标结点链表数据反向插入到新数组下标结点。这个操作可能导致在多线程环境下导致回环链表。[疫苗:JAVA HASHMAP的死循环](https://coolshell.cn/articles/9606.html)
// to[i] = e->f->null;
// t[i] = e->null
// f->e->null;
// t[i] = f->e->null
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
get
// 通过键获取值
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
// 获取key的hash值
int hash = (key == null) ? 0 : hash(key);
// 根据hash值得到数组下标。对数组下标结点链表进行遍历
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
remove
// 根据键删除键值对
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
// 前驱节点
Entry<K,V> prev = table[i];
// 当前节点
Entry<K,V> e = prev;
// 遍历链表
while (e != null) {
// 当前节点的后继节点
Entry<K,V> next = e.next;
Object k;
// 找到要删除的节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
// 要删除的结点是第一个结点,数组第一个元素指向当前节点的后继节点
if (prev == e)
table[i] = next;
else
// 将删除节点的前驱的后继链接到删除结点的后继节点
prev.next = next;
e.recordRemoval(this);
// 返回删除元素结点
return e;
}
// 更新前驱和当前节点
prev = e;
e = next;
}
return e;
}