业精于勤荒于嬉,行成于思毁于随;
在码农的大道上,唯有自己强才是真正的强者,求人不如求己,静下心来,开始思考…
今天一起来聊一聊 HashMap集合,看到这里,笔者懂,大家莫慌,先来宝图镇楼 ~
咳咳… 对于屏幕前帅气的猿友们来说,HashMap… 张口就来,闭眼能写,但是呢,面试官大大一问立马慌,自己阅读源码时,又隐隐觉得知其然不知其所以然,但直面高薪诱惑,又不得不研究,如此甚难;
那么…此时,笔者帅气的脸庞似有似无洋溢起一抹微笑,毕竟是查看过源码的猿,就是那么的豪横,话不多说,来吧,展示…
大家都知道,jdk1.8版本底层数组+链表(单向链表)+红黑树,结合笔者的经验之谈,我觉得在分析HashMap集合具体操作源码前,有必要先了解下其底层链表结构以及红黑树,话不多说,上源码…
/**
* HashMap1.8中定义- 单向链表
*/
static class Node<K, V> implements Map.Entry<K, V> {
// 当期key对应hash值
final int hash;
// key值
final K key;
// value值
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;
}
public final K getKey() {
return key; }
public final V getValue() {
return value; }
// 重写toString方法
public final String toString() {
return key + "=" + value;
}
// 重写hashCode方法
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 重写equals方法
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;
}
}
我们发现,HashMap1.8版本定义的链表结构之单向链表除了类名从Entry改为Node以外,代码方面与1.7版本的HashMap是一致的…
每一个Node节点也包含四个属性:key表示当前节点key值;value表示当前节点value值,next节点表示当前节点下一个节点,如当前节点为链表末尾节点,则当前节点的next节点为null;hash表示当前节点key值通过算法计算出来的hash值;
抽象图解如下(其实笔者并不是很认同此图能形象的代表链表结构,但抽象理解还是可以的):
/**
* HashMap1.8中定义- 红黑树
*/
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);
}
// ...省略诸多代码,详情请参考源码
}
/**
* 红黑树节点继承与LinkedHashMap定义的Entry节点
*
* **重点:我们发现Enty节点又继承与 HashMap中的Node节点
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
我们发现,红黑树节点继承于LinkedHashMap定义的Entry节点,而Entry节点又继承于HashMap中的Node节点…
HashMap1.8版本底层 数组 + 单向链表 图解:
// 加载因子
final float loadFactor;
// 加载因子 - 默认值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 无参构造
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* 有参构造
* @param initialCapacity :用于计算threshold(扩容阈值)
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 有参构造
* @param initialCapacity:用于计算threshold(扩容阈值)
* @param loadFactor:自定义加载因子
*/
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);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
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;
}
从源码中可以看出,构造只为相关参数(加载因子、扩容阈值)进行初始化;
// 底层数组
transient Node<K, V>[] table;
// 转红黑树 - 阈值
static final int TREEIFY_THRESHOLD = 8;
// 对HashMap操作次数
transient int modCount;
// HashMap存放元素个数
transient int size;
// 底层数组扩容阈值(也可以理解为HashMap底层数组实际存放元素大小)
int threshold;
// 扩容最大值
static final int MAXIMUM_CAPACITY = 1 << 30;
// 底层数组默认容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 标识链表转数组要求数组最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 入口
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 计算key对应hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 添加元素
*
* @param hash:添加元素key值对应hash值
* @param key:添加元素key值
* @param value:添加元素value值
* @param onlyIfAbsent:内容覆盖标识(翻译:如果为真,不要改变现有的值,系统默认为false)
* @param evict:个人理解:标识插入节点后是否进行其他操作,默认为true
* @return 返回前一个值,如果为空则返回null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// 1-1.tab为当前全局table数组
Node<K, V>[] tab;
// 2-2.p为当前添加元素key对应的Node节点/链表
Node<K, V> p;
// 1-2.tab数组长度
int n;
// 2-1.i为当前添加元素key存放tab数组对应的index值[与jdk1.7中计算index值一样,(hash & table.length - 1)]
int i;
// 1.判断当前数组是否为空 是 -> 表示当前第一次put元素
if ((tab = table) == null || (n = tab.length) == 0) {
// 进行对数组及参数初始化
n = (tab = resize()).length;
}
// 2.(未发生index冲突)判断当前添加元素在tab数组对应位置下是否为第一个Node节点 是-> 此位置第一次添加Node节点
if ((p = tab[i = (n - 1) & hash]) == null)
// 创建Node节点赋值到tab数组对应i位置下
tab[i] = newNode(hash, key, value, null);
// 3.(发生index冲突)表示当前tab数组对应位置下为链表/红黑树
else {
// 3.2-1.e为p节点,也就是当前添加元素key对应的Node节点
Node<K, V> e;
// 3.1-1.k为当前节点对应key,也就是当前添加元素key值
K k;
// 3.1.判断当前添加元素对应节点hash一致且key相等内容覆盖(先赋值,后进行新增) ->可知:当前节点为第一个Node节点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 3.2.判断当前节点是否为红黑树类型 ->可知:当前tab对应的位置已为 红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); // 已红黑树方式添加元素
// 3.3.可知:当前tab对应位置为链表
else {
// 遍历链表
for (int binCount = 0; ; ++binCount) {
// 判断当前节点下个节点是否为空 ->可知:当前节点为对应链表最后一个Node节点
if ((e = p.next) == null) {
// 直接在此节点的next节点添加 ->可知:HashMap1.8为尾插法
p.next = newNode(hash, key, value, null);
// 判断链表长度是否大于8 是->链表转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
// 链表转红黑树操作
treeifyBin(tab, hash);
break; // 因源码作者这里定义为死循环,故增加节点跳出
}
// 遍历中判断对应节点hash一致且key相等内容覆盖(先赋值,后进行新增)
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break; // 因源码作者这里定义为死循环,故增加节点跳出
// 赋值p,继续遍历
p = e;
}
}
// 3.4.判断当前e的Node节点不为空,对其内容进行覆盖(经过步骤3进入此判断表示这里为覆盖)
if (e != null) {
// 获取就值
V oldValue = e.value;
// onlyIfAbsent翻译为:如果为真,不要改变现有的值,系统默认为false,所以表示默认为覆盖
if (!onlyIfAbsent || oldValue == null) {
// 内容覆盖
e.value = value;
}
// 设计模式-模板方法-子类实现扩展 -> HashMap无实现 / LinkedHashMap有具体实现
afterNodeAccess(e);
// 返回删除元素旧值
return oldValue;
}
}
// 操作次数++
++modCount;
// HashMap元素个数 > 扩容阈值
if (++size > threshold) {
resize(); // 扩容
}
// 设计模式-模板方法-子类实现扩展 -> HashMap无实现 / LinkedHashMap有具体实现
afterNodeInsertion(evict);
return null;
}
// 具体扩容方法
final Node<K, V>[] resize() {
// 获取全局table数组 (便于理解,定义为旧数组)
Node<K, V>[] oldTab = table;
// 获取数组容量 (便于理解,定义为旧数组容量)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 获取扩容阈值 (便于理解,定义为旧数组扩容阈值)
int oldThr = threshold;
// (便于理解,newCap定义为新数组容量、newThr定义为新数组扩容阈值)
int newCap, newThr = 0;
// 如旧数组容量>0 ->可知:数组已初始化过了
if (oldCap > 0) {
// 旧数组容量>=数组最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
// 扩容阈值为Integer最大值
threshold = Integer.MAX_VALUE;
// 返回旧数组
return oldTab;
}
// 新数组容量(旧数组容量*2) < 扩容最大值 且 旧数组容量 >= 数组默认容量(16) -> 新数组容量:2倍扩容
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
// 新数组扩容阈值:2倍扩容
newThr = oldThr << 1; // double threshold
}
}
// 旧数组扩容阈值>0(当前情况:table数组未初始化,threshold>0) ->可知:此种情况为调用有参构造方法时会进入此判断
else if (oldThr > 0) {
// 新数组容量设置为旧数组扩容阈值,HashMap使用threshold变量暂时保存initialCapacity值
newCap = oldThr;
}
// (当前情况:table数组未初始化,threshold=0) ->可知:当前为第一次put元素,数组及参数进行初始化
else {
// 新数组容量=16
newCap = DEFAULT_INITIAL_CAPACITY;
// 新数组扩容阈值=16*0.75=12
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 新数组扩容阈值为0
if (newThr == 0) {
// ft=新数组容量*加载因子
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) {
// 遍历数组,并将元素映射到新数组中
for (int j = 0; j < oldCap; ++j) {
// e为当前数组位置对应节点
Node<K, V> e;
// 当前节点不为空
if ((e = oldTab[j]) != null) {
// 将循环节点设置为空
oldTab[j] = null;
// 当前节点下个节点为空 ->可知:当前数组位置对应只有一个节点
if (e.next == null) {
// 重新计算当前节点对应新数组下标位置,并赋值
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;
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 {
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;
}
// 创建链表Node节点
Node<K, V> newNode(int hash, K key, V value, Node<K, V> next) {
return new Node<>(hash, key, value, next);
}
// 链表转红黑树
final void treeifyBin(Node<K, V>[] tab, int hash) {
// n为当前全局tab数组长度;index为当前添加元素hash对应数组下标位置
int n, index;
// 用于循环的迭代变量,代表当前节点
Node<K, V> e;
// tab数组为空进行初始化;tab不为空且长度<64未达到转红黑树条件,需继续进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 数组index位置对应链表不为空,进行链表转红黑树
else if ((e = tab[index = (n - 1) & hash]) != null) {
// hd为head头节点,tl为tail尾节点
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);
// hd树不为空赋值给tab数组对应位置
if ((tab[index] = hd) != null) {
// 具体树化操作 - 此知识点(红黑树变色及旋转)笔者之后会另起一篇详解
hd.treeify(tab);
}
}
}
// 创建树TreeNode节点
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
相信任谁跟如此一大坨代码硬杠之下,结局也只有放弃抵抗,包括笔者(同样是硬着头皮看着一根根掉落在键盘上的头发,再放弃与挣扎中承受着,一种欣喜又苦涩的心情),或许,这就是程序猿吧。
不知过了许久…一口叹气声,打破了空气中凝固的气氛…
庆幸的是,帅气的笔者最终还是成功拿下了它,笔者帅气的脸庞似有似无洋溢起一抹微笑,也许,这才是一名合格的程序猿吧。
咳咳…猝不及防的一波感慨人生,看看天花板,生活依旧继续,我们言归正传…
其实呢,相信看过上篇(HashMap1.7版本源码分析)的猿友已或多或少掌握了笔者查看源码的技巧,其实说白了,研究源码过后,就会明白源码作者当时在写代码时的思路以及逻辑,作为暖男的笔者再按照源码作者的思路逻辑进行步骤标记及注释,屏幕前的猿友再去结合笔者的步骤标记以及注释去阅读,是可以起到事半功倍的作用的。
接下来,我们重新审阅put()方法就会发现,其内部其实只做了四件事,下面我们拆解分析下:
这里需注意一点,数组的扩容及初始化都定义在resize(),方法内部通过参数判断进行区分,在这里我们先阐述初始化部分相关代码,最后再进行整体分析,按照暖男笔者的思路来吧…
// tab为当前全局table数组
Node<K, V>[] tab;
// tab数组长度
int n;
// 1.判断当前数组是否为空 是 -> 表示当前第一次put元素
if ((tab = table) == null || (n = tab.length) == 0) {
// 进行对数组及参数初始化
n = (tab = resize()).length;
}
// 新数组容量=16
newCap = DEFAULT_INITIAL_CAPACITY;
// 创建新数组
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
// 将新数组赋值给全局table数组
table = newTab;
// 新数组扩容阈值=16*0.75=12
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
// 底层数组扩容阈值=16
threshold = newThr;
// tab为当前全局table数组
Node<K, V>[] tab;
// p为当前添加元素key对应的Node节点/链表
Node<K, V> p;
// tab数组长度
int n;
// i为当前添加元素key存放tab数组对应的index值[与jdk1.7中计算index值一样,(hash & table.length - 1)]
int i;
// 2.(未发生index冲突)
// 判断当前添加元素在tab数组对应位置下是否为第一个Node节点 是-> 此位置第一次添加Node节点
if ((p = tab[i = (n - 1) & hash]) == null) {
// 创建Node节点赋值到tab数组对应i位置下
tab[i] = newNode(hash, key, value, null);
}
i = (n - 1) & hash:计算当前key对应数组存放的下标位置,我们发现,jdk1.8版本与1.7版本计算下标方式是一样的;
所谓的未发生冲突,也就是当前添加元素的key值,通过一定算法计算出来的index值,对应在数组其位置上还未存在节点,也表示此位置第一次添加Node节点。
// tab为当前全局table数组
Node<K, V>[] tab;
// p为当前添加元素key对应的Node节点/链表
Node<K, V> p;
// 3.(发生index冲突)表示当前tab数组对应位置下为链表/红黑树
else {
// e为p节点,也就是当前添加元素key对应的Node节点
Node<K, V> e;
// k为当前节点对应key,也就是当前添加元素key值
K k;
// 3.1.判断当前添加元素对应节点hash一致且key相等内容覆盖(先赋值,后进行新增) ->可知:当前节点为第一个Node节点
// ...
// 3.2.判断当前节点是否为红黑树类型 ->可知:当前tab对应的位置已为 红黑树
else if (p instanceof TreeNode)
// ...
// 3.3.可知:当前tab对应位置为链表
else
// ...
// 3.4.判断当前e的Node节点不为空,对其内容进行覆盖(主要针对3.1 / 3.3中,执行覆盖操作)
// ...
}
// 3.1.判断当前添加元素对应节点hash一致且key相等内容覆盖 ->可知:当前节点为头节点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
e = p; // (先赋值,最后通过 3.4步骤进行覆盖操作)
}
// 3.2.判断当前节点是否为红黑树类型 ->可知:数组对应的位置为 红黑树
else if (p instanceof TreeNode) {
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); // 已红黑树方式添加元素
}
// 3.3.可知:当前tab对应位置为链表
else {
// 遍历链表
for (int binCount = 0; ; ++binCount) {
// 判断当前节点下个节点是否为空 ->可知:当前节点为对应链表最后一个Node节点
if ((e = p.next) == null) {
// 3.3.2.直接在此节点的next节点添加 ->可知:HashMap1.8为尾插法
p.next = newNode(hash, key, value, null);
// 3.3.3判断链表长度是否大于8 是->链表转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
// 链表转红黑树操作
treeifyBin(tab, hash);
break; // 因源码作者这里定义为死循环,故增加节点跳出
}
// 3.3.1.遍历中判断对应节点hash一致且key相等内容覆盖(先赋值,后进行新增)
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break; // 因源码作者这里定义为死循环,故增加节点跳出
// 赋值p,继续遍历
p = e;
}
}
如遍历过程中,存在与当前添加元素hash值一致且key值相等情况,进行覆盖;
遍历至结束未发现(hash值一致且key值相等的节点),添加当前元素;
接着第2点,添加元素节点后,判断当前链表长度是否大于等于8,如满足,进行树化操作;
注意:执行树化操作需满足两个条件,其上述链表长度大于等于8为其一,再进入treeifyBin()方法内部,真正执行树化操作还需满足 数组长度大于等于64,只有同时满足两者才会真正执行树化操作,也就是我们常说的链表转红黑树。
// 3.4.判断当前e的Node节点不为空,对其内容进行覆盖(经过步骤3进入此判断表示这里为覆盖)
if (e != null) {
// 获取就值
V oldValue = e.value;
// onlyIfAbsent翻译为:如果为真,不要改变现有的值,系统默认为false,所以表示默认为覆盖
if (!onlyIfAbsent || oldValue == null) {
// 内容覆盖
e.value = value;
}
// 设计模式-模板方法-子类实现扩展 -> HashMap无实现 / LinkedHashMap有具体实现
afterNodeAccess(e);
// 返回删除元素旧值
return oldValue;
}
到此,HashMap整个put()方法的框架思路已讲解完毕,相信对于屏幕前的猿友们来说,是不是很 so easy…
此时,笔者嘴角若隐若现一丝弧度微起,源码么,也不过如此…
言归正传,虽然跟着笔者的思路,熟悉了put()方法的整体框架思路,但是凭此去面对面试官的话,还是略显单薄…
在我们Java面试界有一个不成文的规定,面试官只要提到HashMap,肯定会问其底层结构实现以及重中之重的扩容,不过对于有经验的开发猿来说,面试官只需询问:“你了解HashMap么?”,那么好,从hashMap1.7版本底层结构到扩容,再从头插法引出扩容死循环的问题以及时间/空间复杂度导致查询效率低的问题,行云流水般的过渡到hashMap的jdk1.8版本,相似的套路,从底层结构到其扩容,巧妙的运用尾插法解决扩容死循环问题,及加入红黑树,大大提升查询效率,最后再从线程安全问题过渡到ConcurrentHashMap,一套进可攻退可守的闪电五连鞭打在面试官一脸懵B的脸上,可想而知,此时此刻,You就是整个会议室最靓的仔。 细心的猿友是不是已经默默掏出小本本开始记知识点了呢,学知识就是要不讲武德!
那么好,接下来就来谈谈其 扩容及树化操作…
// 具体扩容方法
final Node<K, V>[] resize() {
// 获取全局table数组 (便于理解,定义为旧数组)
Node<K, V>[] oldTab = table;
// 获取数组容量 (便于理解,定义为旧数组容量)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 获取扩容阈值 (便于理解,定义为旧数组扩容阈值)
int oldThr = threshold;
// (便于理解,newCap定义为新数组容量、newThr定义为新数组扩容阈值)
int newCap, newThr = 0;
// 2.如旧数组容量>0 ->可知:数组已初始化过了
if (oldCap > 0) {
// 旧数组容量>=数组最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
// 扩容阈值为Integer最大值
threshold = Integer.MAX_VALUE;
// 返回旧数组
return oldTab;
}
// 新数组容量(旧数组容量*2) < 扩容最大值 且 旧数组容量 >= 数组默认容量(16) -> 新数组容量:2倍扩容
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
// 新数组扩容阈值:2倍扩容
newThr = oldThr << 1; // double threshold
}
}
// 3.旧数组扩容阈值>0(当前情况:table数组未初始化,threshold>0) ->可知:此种情况为调用有参构造方法时会进入此判断
else if (oldThr > 0) {
// 新数组容量设置为旧数组扩容阈值,HashMap使用threshold变量暂时保存initialCapacity值
newCap = oldThr;
}
// 1.(当前情况:table数组未初始化,threshold=0) ->可知:当前为第一次put元素,数组及参数进行初始化
else {
// 新数组容量=16
newCap = DEFAULT_INITIAL_CAPACITY;
// 新数组扩容阈值=16*0.75=12
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// (针对上述情况3)新数组扩容阈值为0
if (newThr == 0) {
// ft=新数组容量*加载因子
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) {
// 遍历数组,并将元素映射到新数组中
for (int j = 0; j < oldCap; ++j) {
// e为当前数组位置对应节点
Node<K, V> e;
// 当前节点不为空
if ((e = oldTab[j]) != null) {
// 将循环节点设置为空
oldTab[j] = null;
// 当前节点下个节点为空 ->可知:当前数组位置对应只有一个节点
if (e.next == null) {
// 重新计算当前节点对应新数组下标位置,并赋值
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;
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 {
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;
}
还是一样,按照上述的步骤标记及注释,跟随笔者的思路一起来分析下:
// 获取全局table数组 (便于理解,定义为旧数组)
Node<K, V>[] oldTab = table;
// 获取数组容量 (便于理解,定义为旧数组容量)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 获取扩容阈值 (便于理解,定义为旧数组扩容阈值)
int oldThr = threshold;
// (便于理解,newCap定义为新数组容量、newThr定义为新数组扩容阈值)
int newCap, newThr = 0;
// (当前情况:table数组未初始化,threshold=0) ->可知:当前为第一次put元素,数组及参数进行初始化
else {
// 新数组容量=16
newCap = DEFAULT_INITIAL_CAPACITY;
// 新数组扩容阈值=16*0.75=12
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
可知:第一次put元素,数组进行初始化,容量为16,扩容阈值为12;
// 如旧数组容量>0 ->可知:数组已初始化过了
if (oldCap > 0) {
// 旧数组容量>=数组最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
// 扩容阈值为Integer最大值
threshold = Integer.MAX_VALUE;
// 返回旧数组
return oldTab;
}
// 新数组容量(旧数组容量*2) < 扩容最大值 且 旧数组容量 >= 数组默认容量(16) -> 新数组容量:2倍扩容
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
// 新数组扩容阈值:2倍扩容
newThr = oldThr << 1; // double threshold
}
}
可知:数组扩容最大值为1073741824(1 << 30),扩容阈值最大值为Integer最大值2147483647(2^31-1),扩容后为为之前数组长度的2倍;
// 旧数组扩容阈值>0(当前情况:table数组未初始化,threshold>0) ->可知:此种情况为调用有参构造方法时会进入此判断
else if (oldThr > 0) {
// 新数组容量设置为旧数组扩容阈值,HashMap使用threshold变量暂时保存initialCapacity值
newCap = oldThr;
}
// (此判断针对 当前情况 3)新数组扩容阈值为0,进入此判断
if (newThr == 0) {
// ft=新数组容量*加载因子
float ft = (float) newCap * loadFactor;
// 计算新数组扩容阈值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE);
}
此种情况为调用有参构造实现,内部通过tableSizeFor(initialCapacity)方法计算扩容阈值;
可知:此情况数组容量值为扩容阈值,当数组容量小于数组最大值 且 (ft = 数组容量*加载因子)小于数组最大值,则扩容阈值为 ft,反之为Integer最大值2147483647(2^31-1),其中加载因子支持用户自定义,默认值为0.75;
// 扩容阈值设置为新数组扩容阈值
threshold = newThr;
// 创建新数组,长度为新数组容量
@SuppressWarnings({
"rawtypes", "unchecked"})
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
// 将新数组赋值给全局table数组
table = newTab;
// 旧数组不为空,进行遍历分组重新映射到新数组中
if (oldTab != null) {
// 遍历数组,并将元素映射到新数组中
for (int j = 0; j < oldCap; ++j) {
// e为当前数组位置对应节点
Node<K, V> e;
// 当前节点不为空
if ((e = oldTab[j]) != null) {
// 将循环节点设置为空
oldTab[j] = null;
// 1.当前节点下个节点为空 ->可知:当前数组位置对应只有一个节点
if (e.next == null) {
// 重新计算当前节点对应新数组下标位置,并赋值
newTab[e.hash & (newCap - 1)] = e;
}
// 2.可知:当前数组位置对应为红黑树
else if (e instanceof TreeNode) {
// 红黑树拆分
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
}
// 3.可知:当前数组位置对应为链表
else {
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) {
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;
通过笔者代码标记注释,可以看到映射值分为三种方式:
当前数组位置对应只有一个节点,直接重新计算对应新数组的位置并赋值;
当前数组位置对应为红黑树节点,进行红黑树拆分并赋值新数组中;
当前数组位置对应为链表,遍历链表重新计算每一个节点对应新数组的位置并赋值;
到这里,put()方法就结束了,相信屏幕前的猿友们或多或少受益匪浅;
学习亦是如此,当你翻过代码中最高的的一座山之后,剩下的只是一码平川;
猿友们此时此刻是不是正干劲十足呢,拿下HashMap近在咫尺喽。
/**
* 入口
*/
public V get(Object key) {
Node<K,V> e;
// 不得不说,1.8版本的作者代码功底很nice,个人很喜欢
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 获取对应节点方法
final Node<K, V> getNode(int hash, Object key) {
// 当前全局table
Node<K, V>[] tab;
// first为当前第一个节点;e用于循环的迭代变量,代表当前节点
Node<K, V> first, e;
// tab数组长度
int n;
// first节点的key值
K k;
// 如果当前tab不为空 && tab长度>0 && 当前数组对应下标位置节点不为空
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 1.与first节点hash值相等 && 与first节点key值相等,则返回first节点
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
// first.next不为空,继续遍历查找
if ((e = first.next) != null) {
//2. 红黑树方式获取
if (first instanceof TreeNode)
return ((TreeNode<K, V>) first).getTreeNode(hash, key);
// 3.遍历链表获取
do {
// 与当前节点hash值相等 && 与当前节点key值相等,则返回当前节点
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null; // 未找到返回null
}
相信对于屏幕前已拿下put()方法的你来说,获取元素方法,简直very so easy;
从源码中可以看出,获取元素时分为四种方式:
通过key计算对应数组下标从而获取对应节点(first节点),当获取元素与first节点hash值相等 且 key值相等,则返回first节点,最终返回其value值;
当与first节点对比不一致且first节点的next节点不为空时,并且为红黑树节点时,已红黑树方式获取节点并返回,最终返回其value值;
当与first节点对比不一致且first节点的next节点不为空时并且不为红黑树节点时,表示当前为链表,遍历链表进行对比,如存在与当前节点hash值相等 且 key值相等,则返回当前节点,最终返回其value值:
如未找到,则返回null;
/**
* 入口
*/
public V remove(Object key) {
Node<K,V> e;
// removeNode()为具体删除节点方法
return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}
// 删除元素方法
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
// 当前全局table
Node<K, V>[] tab;
// 当前数组对应位置的节点/链表/红黑树
Node<K, V> p;
// n为tab数组长度;index当前删除元素所在数组下标位置
int n, index;
// 当前tab(全局数组)不为空 && tab数组长度>0 && 当前数组对应下标位置节点不为空
if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
// node为返回的删除节点;e用于循环的迭代变量,代表当前节点
Node<K, V> node = null, e;
// 当前节点对应key值
K k;
// 当前节点对应value值
V v;
/**
* 1.获取删除节点
*/
// (当前p为头结点,node节点也为头结点)与头节点hash值相等 && 与头节点key值相等 ->可知:删除节点为头节点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// p.next不为空,继续遍历查找
else if ((e = p.next) != null) {
// 红黑树 - 获取对应删除节点
if (p instanceof TreeNode)
node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
// 遍历链表 - 获取对应删除节点
else {
do {
// 与当前节点hash值相等 && 与当前节点key值相等,则为删除节点
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
// **如进入此判断 ->可知:node节点为p.next节点
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
/**
* 2.删除节点
*/
// 查找的删除节点不为空 && (matchValue默认为false,!matchValue为true || value相等)
if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
// 红黑树方式删除节点
if (node instanceof TreeNode)
((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
// node==p表示删除为链表头结点,p与node都为头结点
else if (node == p)
// 将头节点的next节点赋值给tab数组index下标位置处
tab[index] = node.next;
// 当前node节点为p.next节点
else
// 将p的下个节点赋值为node节点的下个节点,因删除节点为node,需改变其前后节点对应引用关系
p.next = node.next;
// 操作次数++
++modCount;
// HashMap元素个数--
--size;
// 设计模式-模板方法-子类实现扩展 -> HashMap无实现 / LinkedHashMap有具体实现
afterNodeRemoval(node);
// 返回删除节点
return node;
}
}
// 未找到返回null
return null;
}
从笔者源码标记中可以看出,删除元素主要分为2步:
/**
* 1.获取删除节点
*/
// (当前p为头结点,node节点也为头结点)与头节点hash值相等 && 与头节点key值相等 ->可知:删除节点为头节点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// p.next不为空,继续遍历查找
else if ((e = p.next) != null) {
// 红黑树 - 获取对应删除节点
if (p instanceof TreeNode)
node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
// 遍历链表 - 获取对应删除节点
else {
do {
// 与当前节点hash值相等 && 与当前节点key值相等,则为删除节点
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
// **如进入此判断 ->可知:node节点为p.next节点
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
此代码与get()获取元素方法一致,在此不再赘述;
/**
* 2.删除节点
*/
// 查找的删除节点不为空 && (matchValue默认为false,!matchValue为true || value相等)
if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
// 红黑树方式删除节点
if (node instanceof TreeNode)
((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
// node==p表示删除为链表头结点,p与node都为头结点
else if (node == p)
// 将头节点的next节点赋值给tab数组index下标位置处
tab[index] = node.next;
// 当前node节点为p.next节点
else
// 将p的下个节点赋值为node节点的下个节点,因删除节点为node,需改变其前后节点对应引用关系
p.next = node.next;
// 操作次数++
++modCount;
// HashMap元素个数--
--size;
// 设计模式-模板方法-子类实现扩展 -> HashMap无实现 / LinkedHashMap有具体实现
afterNodeRemoval(node);
// 返回删除节点
return node;
}
此代码与get()获取元素方法一致,在此不再赘述;
// 红黑树方式删除节点
if (node instanceof TreeNode) {
((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
}
// node==p表示删除为链表头结点,p与node都为头结点
else if (node == p) {
// 将头节点的next节点赋值给tab数组index下标位置处
tab[index] = node.next;
}
// 当前node节点为p.next节点(这里不理解的猿友,请结合上述步骤1中-遍历链表获取对应删除节点中p节点与node节点的关系)
else {
// 将p的下个节点赋值为node节点的下个节点,因删除节点为node,需改变其前后节点对应引用关系
p.next = node.next;
}
// 操作次数++
++modCount;
// HashMap元素个数--
--size;
// 设计模式-模板方法-子类实现扩展 -> HashMap无实现 / LinkedHashMap有具体实现
afterNodeRemoval(node);
// 返回删除节点 - 如未找到->返回null
return node;
到这里… 恭黑雷(恭喜你),顺利拿下HashMap;
此时此刻,屏幕前拥有盛世美颜的你,给也同样拥有盛世美颜的暖男笔者,赏脸来个三连吧…笔者已迫不及待准备好么么哒,亲在…;
- 底层为数组 + 链表(单向链表) + 红黑树 (Red-Black Trees 笔者之后会另起一篇详解);
- 线程不安全;
- 数组初始容量为16,最大值为1073741824(1 << 30),数组扩容为之前数组的2倍;
- 扩容加载因子为0.75,用户也可通过有参构造指定加载因子;
- 扩容阈值为数组容量*加载因子,默认初始扩容阈值为16 * 0.75 = 12;
- 尾插法 - 有效解决扩容死循环问题(1.7版本为头插法);
- 有modCount;
面试一次问一次,HashMap是该拿下了之 HashMap1.7版本
面试一次问一次,HashMap是该拿下了之 ConcurrentHashMap
大家好,我是猿医生:
在码农的大道上,唯有自己强才是真正的强者,求人不如求己,静下心来,扫码一起学习吧…