源码阅读全文
HashMap
基础概念
- 一个元素 -> 计算哈希值 -> 映射成下标 -> 存入哈希表
- 哈希值,通过散列算法,将不固定长度的输入,转换成一个统一长度的输出,且是唯一的,元素的身份证
- 哈希表,存储哈希值的数组,将哈希值通过某种方法,映射成数组长度内的下标
- 哈希函数,一个运算规则,将哈希值通过
哈希函数映射成index - 哈希冲突/碰撞,两个元素通过哈希函数运算后,产生的下标一样
- 重哈希,重新计算哈希值并得到下标,解决哈希冲突
- 开放地址,往下找或者往上找,在相同下标的旁边找空位置,靠数组空位解决,
ThreadLocalMap- 益出区,开辟额外的空间,放产生哈希冲突的元素
- 链地址法,通过链的方式连接哈希冲突的元素,jkd8之后,在链长度多之后,转换成红黑树
- 如果元素均匀得存储在角标位,不产生冲突,就是效率最高的
- 尽可能少产生哈希冲突,哈希函数计算出来的角标,需要尽可能均匀
- 通过数据结构解决,提高效率
哈希函数
jdk1.7
- 结合容量为
2的幂次方,-1后,保证每位都是1,有可能出现
static int indexFor(int h, int length) {
return h & (length-1);
}
length长度规则,必须是2的幂次方,初始默认容量为16,让下标充分利用
- 保证参与下标都是1,容量为
16 10000,对应下标15 1111,任何数 & 上(len - 1)就在0000 ~ 1111之间,也就是长度为16的角标- 如果不是
2的幂次方。奇数5,参与下标为4 100,任何数&上都是偶数,也就是只能为4,奇数位角标永远没有值,数组空间浪费;偶数6,参与下标为5 101,中间也有一个0,永远不能取到3 011的下标- 扩容后是原长度的两倍,在扩容时,需要复制一份,也可以保证迁移后的数据均匀分布,例如原来的容量为
8,扩容为16,哈希低四位为1111,原来的下标为5 101,新下标为1101 13,也就是原来的下标+扩容长度,要么就在原角标位(低四位 0111)- jdk1.7重新哈希,1.8没有
![]()
- 通过
roundUpToPowerOf2保证初始化长度为2的幂次方,在第一次put的时候调用
private static int roundUpToPowerOf2(int number) {
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
//取-1后的最高位,后面全部补零 1010减一 -> 1001 9 -> 10000 16
//1111 -> 1110 -> 10000
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
jdk1.8
- 同样是通过模的方式取下标
- 通过
tableSizeFor保证初始化长度为2的幂次方,保证输入的所有数最后每位都能为1,加1后,就是2的幂次方了
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;
}
添加节点
jdk1.7
- 头插法
hash, key, value, e,分别为 hash值/key/value/next节点
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
jdk1.8
- 当前角标为null,直接加入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
- 单向链表的插入
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //当前下标元素的next为null,直接尾插法
p.next = newNode(hash, key, value, null);
//该角标,头节点被处理过了
//默认为8,只要当前角标下的链表长度>=8了,将该角标的链表转换成红黑树
//概率学的问题,长度为8;进行树化的概率很低,尽量避免树化
//并且8以下的长度,获取和查找的性能都是可以接受的
//红黑树一个节点的空间占用是普通的两倍,只有长度大于8时,才值得去树化,以空间换时间
//当极端情况下,会导致哈希碰撞概率升高,会进行树化,以提高查找效率
//但是理想情况的hash,达到8的概率很小
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//转换成红黑树
treeifyBin(tab, hash);
break;
}
//找到相同hash,相同的值,替换旧值
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;
}
- 树化,需要满足两个条件,
表长>=64 && 角标元素个数>=8
- 先形成双向链表
- 再进行红黑树化
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//对表长度有要求,默认64,表长度不达标也不会尝试树化,通过扩容的方式解决
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//用TreeNode包装
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
//记录首元素
hd = p;
else {
p.prev = tl; //下一个节点指向上一个节点
tl.next = p; //上一个节点指向下一个节点
}
tl = p; //相当于指针,指针后移
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab); //树化,利用hash值构建二叉搜索树(红黑树),并进行平衡
}
}
treeify
扩容
- 遇到哈希冲突,通过扩容解决,
节点个数大于阈值时扩容 - 1.7之前通过单向链表,1.8之后通过
单向链表 7个节点/红黑树 8个节点
jdk1.7
- 添加元素使用头插法
- 什么时候扩容,
put方法中,如果找到了hash相等且key相等,那么就替换值,并且直接返回旧值,否则如果容量超过阈值,需要扩容,最后添加元素
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
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 || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//记录修改的次数,和线程安全有关
//Fail-Fast 机制,当多个线程对同一集合的内容进行操作时,发现`exceptModCount`不一样,会抛出异常
modCount++;
addEntry(hash, key, value, i);
return null;
}
- 阈值默认是
size * 0.75 - 怎么扩容
void addEntry(int hash, K key, V value, int bucketIndex) {
//需要扩容且对应角标不为null
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
//重新计算hash和角标,再次添加
bucketIndex = indexFor(hash, table.length);
}
//如果不需要扩容,或对应角标为null,直接增加
//或者扩容完再添加
createEntry(hash, key, value, bucketIndex);
}
transfer实际进行迁移工作,重新计算元素下标,扩容数据迁移时,是否重哈希要看参数rehash
单向链表的迁移,头插法,将新元素作为
head
- 扩容后问题(多线程下),头插法会导致形成环形链表
- 原数据
10->2->6,线程1执行到next=e.next,线程1中断,线程2执行完成2->10,线程1继续执行,也就形成了10->2的环形情况
jdk1.8
- 添加元素使用尾插法
- 如果角标位是单向链表,将单向链表进行数据迁移
- 如果角标位是红黑树,通过其中的双向链表结构进行数据迁移
- 阈值初始值由
tableSizeFor进行初始化,初始化最近的2的整数次幂 - 放入第一个元素,才有真正的大小
- 后续为
newCap * 0.75的大小 - 什么时候扩容,先添加元素,判断
节点个数是否达到阈值,超过直接扩容
if (++size > threshold)
resize();
- 怎么扩容
- 容量扩充两倍,更新阈值
- 迁移元素,并根据是否能放在高位,将原下标的节点分开存放
- 扩容时,没有重哈希,看元素最高位与原长度的关系,
resize进行迁移工作
- 因为迁移时,涉及到链表/树结构,需要检查每个节点的情况
- 尾插法,将新元素作为
Tail
- 针对于单向链表的迁移,
(e.hash & oldCap) == 0用lo保存,放在原位置;不为0,用hi保存,放在高位(原下标+原长度) - 如果是
红黑树结构,通过TreeNode中维护的双向链表,进行迁移 - 通过类似单向链表的迁移方式,进行迁移,过程中需要记录节点个数,判断是否需要再次进行树化
e.next = null;和(e.prev = loTail) == null的操作,同时断掉了原先双向链表的结构,形成了新的双向链表
- 是否需要生成红黑树结构的判断逻辑,迁移之后数量小于
6,转换回为单向链表
if (loHead != null) {
//UNTREEIFY_THRESHOLD 默认为6
if (lc <= UNTREEIFY_THRESHOLD)
//将节点转换回Node
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
//如果为null,那么就不要重新树化,所有节点都在原位置
//如果不为null,那么树中的某些节点迁移到了高位,需要重新树化
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
- 扩容后问题(多线程下)
存在数据丢失、值的覆盖,多线程操作一个角标
获取节点
jdk1.7
get
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
getEntry
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
//遍历链表查找
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;
}
jdk1.8
get
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
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;
}
删除节点
jdk1.7
remove
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
- 删除逻辑
recordRemoval为空实现,模版方法设计模式,LinkedHashMap中有实现
jdk1.8
remove
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
- 删除逻辑
- 实际删除
节点
jdk1.7
- 构造,需要传入
next,配合头插法
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
jdk1.8
Node
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
TreeNode,多了left/right/prev/red,同时维护红黑树和双向链表
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
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);
}
区别
- 数据结构不同
- jdk1.7,单向链表
- jdk1.8,7个节点单向链表,8个节点或以上红黑树
- 插入方法不同
- jdk1.7,头插法,多线程下形成环形链表
- jdk1.8,尾插法,多线程下容易导致节点覆盖,数据丢失
- 扩容
- jdk1.7,先扩容,再添加
- jdk1.8,先添加(过程中判断是否需要树化),再扩容(过程中检查是否需要还原回单向链表)
- 数据迁移的不同
- jdk1.7,待添加元素需要重哈希,其他元素可能需要重哈希,重新计算元素下标
- jdk1.8,直接看
hash & 原高度,是否需要+原高度