目录
3.2 当前位置Node与新元素的key、hash不相同,且当前位置Node不是树节点
3.3 链表长度大于等于8,且数组长度大于等于64时,转化为红黑树
总结:
HashMap空参构造,只初始化了负载因子(0.75),其他成员变量均为默认值。
常用的有参构造方法 HashMap(int initialCapacity),是可以设置初始化大小的,在大概知道需要多大的map时,可以考虑使用这个构造方法。
HashMap 扩容:每次扩容至原来的2倍。
使用空参构造创建的对象,在第一次添加元素的时候,才会初始化一个长度为16的Node类型的数组。
链表转红黑树的时机:链表长度大于8 , 数组长度大于64
红黑树转链表的时机:链表程度小于 6
HashMap 允许空值作为键和值
HashMap 是无序,且键不重复的
HashMap 线程不安全,多线程操作下可能会抛出 ConcurrentModificationException
1 对象初始化常用构造方法
- HashMap()
// 负载因子(阈值 = 数组长度 * 负载因子),阈值时数组扩容的临界值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 构造一个具有默认的初始容量(16)和默认的负载因子(0.75)的空的HashMap
* (在使用这个构造方法new对象时,并没有创建数组,而是在第一次调用put方法的时候才创建)
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 只初始化负载因子,其它字段使用默认值
}
- HashMap( int initialCapacity )
/**
* 构造一个具有指定的初始容量和默认的加载因子(0.75)的空的HashMap
*
* @param initialCapacity 指定初始容量
* @throws IllegalArgumentException 如果指定的初始容量是负的
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR); // 调用两个参数的构造方法
}
2 第一次调用 put 方法(使用空参构造方法创建的)
- 总体调用链路代码图示
- 源码详细解析
public class HashMap {
// [......]
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// [......]
public HashMap() {
// 初始化 负载因子 默认是0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
// 第一次调用 put 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 实现了 Map.put 及相关方法
*
* @param hash key 的 hash 值
* @param key 键
* @param value 值
* @param onlyIfAbsent 如果是true,就不修改已存在的值
* @param evict 如果是false,table 就是创建模式
* @return 旧值,如果没有就返回null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
// tab=null; 条件成立,进入
if ((tab = table) == null || (n = tab.length) == 0)
// tab = 长度为16的数组; n = 16;
n = (tab = resize()).length;
// i=hashCode p = tab[hashCode] 此处数组中还全部为null,所以 p == null 成立
if ((p = tab[i = (n - 1) & hash]) == null)
// 创建节点,并将其放入bucket(数组)中
tab[i] = newNode(hash, key, value, null);
else {
// [......]
}
++modCount; //此HashMap在结构上修改的次数
if (++size > threshold) // size=1; threshold=12; size > threshold 不成立
resize();
afterNodeInsertion(evict); // LinkedHashMap 的回调函数(此处先不需要管)
return null; // 元素直接填入数组中,所以不存在旧值
}
// 创建一个常规、非树节点
Node<K, V> newNode(int hash, K key, V value, Node<K, V> next) {
return new Node<>(hash, key, value, next);
}
// 初始化、扩容等操作的方法
final Node<K, V>[] resize() {
Node<K, V>[] oldTab = table; // oldTab = null; 现在table数组还没有初始化
int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldTab==null 成立 => oldCap=0;
int oldThr = threshold; // oldThr = threshold = 0
int newCap, newThr = 0;
if (oldCap > 0) { // oldCap目前 = 0 不成立
// [......]
} else if (oldThr > 0) // oldThr目前 = 0 不成立
// [......]
else{ // 最终走的是这个分支:初始阈值为零表示使用默认值
newCap = DEFAULT_INITIAL_CAPACITY; // newCap=16
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // newThr = 0.75 * 16 = 12
}
if (newThr == 0) { // 现在 newThr=12 条件不成立
// [......]
}
threshold = newThr; // threshold=12
@SuppressWarnings({"rawtypes", "unchecked"})
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap]; // 创建数组(bucket)
table = newTab; // table=数组, 到这里,长度为16的数组才初始化完毕
if (oldTab != null) { // oldTab是=null的,这里不成立
// [......]
}
return newTab; // 返回 新数组
}
// Node 节点,存储在数组与链表中(红黑树中存储的是TreeNode)
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) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// [......]
}
}
- 过程图示
(演示中的hash值均为假设值)
调用 new HashMap() 的时候,还没有创建数组。
在第一次调用put方法的时候,比如说:map.put( 1, "abc" );
创建一个Node节点:
创建一个长度为 16 的 Node 类型的数组:
根据公式计算出新Node节点的位置:(16 - 1) & hash = 4 , 将新结点放入数组中
3 put
3.1 如果当前数组位置上Node的键与新的键相同
- 源码详细解析:
public class HashMap {
// [......]
// 调用此方法(假设)
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)
n = (tab = resize()).length;
// 进行到这一步,数组这个位置上已经有值了
if ((p = tab[i = (n - 1) & hash]) == null)
// [......]
else{
Node<K, V> e; // 用来记录是否存在key对应的Node节点,不存在,此值一直为null
K k;
// p 现在是数组当前位置上的node节点,hash 是要添加的元素的key的hash值
// 1、p.hash == hash && ((k = p.key) == key : 如果两个Node节点是相同的,则为 true
// 2、(key != null && key.equals(k))) : 如果两个Node节点的key是相同的,则为 true
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; //进来了,说明当前位置上存在key对应的Node节点p
else if (p instanceof TreeNode)
// [......]
else{
// [......]
}
if (e != null) { // 现有的key映射 此时e!=null满足
V oldValue = e.value; // 记录原值
// 判断如果onlyIfAbsent是false 或者 原值是null , 就修改原值
if (!onlyIfAbsent || oldValue == null)
e.value = value; // 修改值
afterNodeAccess(e);
return oldValue; // 返回原值
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
// [......]
}
- 过程图示:
1、数组中当前状态:
2、map.put( "123", "def" );
3、修改值
注意:在上述过程中,没有产生新的 Node 对象。
3.2 当前位置Node与新元素的key、hash不相同,且当前位置Node不是树节点
- 源码详细分析:
public class HashMap {
// [......]
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)
// [......]
if ((p = tab[i = (n - 1) & hash]) == null)
// [......]
else {
Node<K, V> e;
K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
// [......]
else if (p instanceof TreeNode)
// [......]
else{
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { // 循环遍历链表,如果当前节点的next指针为null ,就将新节点挂载到当前节点
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 判断是否转为红黑树,这里先不深入
treeifyBin(tab, hash);
break;
}
// 判断键是否相同、是否是同一个Node节点,,和上一层的if的目的相同
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // 现有的key映射 此时e!=null满足
V oldValue = e.value; // 记录原值
// 判断如果onlyIfAbsent是false 或者 原值是null , 就修改原值
if (!onlyIfAbsent || oldValue == null)
e.value = value; // 修改值
afterNodeAccess(e);
return oldValue; // 返回原值
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
// [......]
}
- 过程图示:
1、数组中当前状态
2、map.put( "56789", "aaa" );
3、创建新节点并挂载
3.3 链表长度大于等于8,且数组长度大于等于64时,转化为红黑树
源码加图解析:
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)
// [......]
if ((p = tab[i = (n - 1) & hash]) == null)
// [......]
else {
Node<K, V> e;
K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
// [......]
else if (p instanceof TreeNode)
// [......]
else{
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { // 循环遍历链表,如果当前节点的next指针为null ,就将新节点挂载到当前节点
p.next = newNode(hash, key, value, null); // 挂载新节点
if (binCount >= TREEIFY_THRESHOLD - 1) // 判断是否转为红黑树 TREEIFY_THRESHOLD=8
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// [......]
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
在链表长度大于等于8的时候,调用treeifyBin(tab, hash)方法:
假设当前HashMap存储元素如下:
调用 treeifyBin(tab, hash)方法:
//替换指定哈希表的索引处bin中的所有链接节点,除非表太小,否则将改为红黑树。
final void treeifyBin(Node<K, V>[] tab, int hash) {
int n, index;
Node<K, V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // MIN_TREEIFY_CAPACITY=64
resize(); // 如果hash表为null 或者 长度不足64 就不转为红黑树;扩容机制后面再说
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K, V> hd = null, tl = null;
do {
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);
}
}
// 创建一个树节点
TreeNode<K, V> replacementTreeNode(Node<K, V> p, Node<K, V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
// 树节点
static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {
// final int hash; 继承自HashMap.Node
// final K key; 继承自HashMap.Node
// V value; 继承自HashMap.Node
// Node<K,V> next; 继承自HashMap.Node
// Entry<K,V> before, after; 继承自LinkedHashMap.Entry
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);
}
}
我们先将关注点放在这个链表上:
现在先用图详细说一下do while循环中的操作做了什么:
第一次循环:
第二次循环:
第三次循环:
以此类推,最后一次循环之后:循环结束,实际上是创建了一个TreeNode节点的双向链表。
tab[index] = hd 这句代码,将整条TreeNode节点构成的双向链表头放到数组中去。
执行 hd.treeify( tab ) 这个方法,就是将双向链表转换为红黑树。
双向链表转红黑树就不继续剖析了,后续会专门对红黑树结构进行总结。
可以参考:https://blog.csdn.net/weixin_40255793/article/details/80748946
还剩下一些红黑树的操作没有详细展开,后续专门对红黑树进行总结的时候再进行补充。
4 扩容
// 扩容 这里分析第一次扩容 假如现在是插入第13个元素
final Node<K, V>[] resize() {
Node<K, V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; //oldCap=16
int oldThr = threshold; // oldThr=12
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) { // MAXIMUM_CAPACITY=1<<30
// [......]
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// newCap=oldCap<<1 = 16 * 2 = 32 每次扩容为原来的2倍
// oldCap(16) >= DEFAULT_INITIAL_CAPACITY(16) 成立
newThr = oldThr << 1; // newThr=24
} else if (oldThr > 0)
// [......]
else{
// [......]
}
if (newThr == 0) {
// [......]
}
threshold = newThr;
@SuppressWarnings({"rawtypes", "unchecked"})
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap]; // new 一个长度为32的数组
table = newTab;
if (oldTab != null) { // 如果原hash表不为null,就将旧的HashMap中的所有元素分配到新的HashMap
for (int j = 0; j < oldCap; ++j) { // 遍历处理每一个桶
java.util.HashMap.Node<K, V> e;
if ((e = oldTab[j]) != null) { // 取出当前桶中的元素
oldTab[j] = null; // 将原桶位置引用置为null
if (e.next == null) //如果当前桶中只有一个元素,直接重新hash
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof java.util.HashMap.TreeNode) // 如果是红黑树,就切分成两个红黑树
((java.util.HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);
else { // 如果是单链表,就拆分成2个单链表
java.util.HashMap.Node<K, V> loHead = null, loTail = null; // 原索引处的一个链表
java.util.HashMap.Node<K, V> hiHead = null, hiTail = null; // 原索引+oldCap的一个链表
java.util.HashMap.Node<K, V> next;
do {
next = e.next;
// 根据hash值的某一位为0还是1将单链表拆分
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 将拆分后的两个链表放到新链表的对应索引处
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
注意:
如果是单个节点,直接重新计算位置放下即可。
如果是链表,则根据 e.hash & oldCap 是0还是1分成两个单链表,一个放在原索引位置,一个放在 index+oldCap 位置。
如果是红黑树,则根据 e.hash & bit 是0还是1分成两个红黑树,一个放在原索引位置,一个放在 index + bit 位置。如果红黑树元素小于6了,就会转换成链表。
上述操作是 HashMap 的优化部分,在每一次扩容后,可以将原素分散开放在 HashMap 中。
源码分析参考:https://blog.csdn.net/weixin_40255793/article/details/80748946