导读
前面文章一、深入理解-Java集合初篇 中我们对Java的集合体系进行一个简单的分析介绍,上一篇文章二、Jdk1.7和1.8中HashMap数据结构及源码分析 中我们对JDK1.7中HashMap的数据结构、主要声明变量、构造函数、HashMap的put操作方法做了深入的讲解和分析,本篇文章是上一篇文章的后续。本篇文章我们将要对JDK1.8中HashMap的数据结构、主要声明变量、构造函数、HashMap的put操作方法等做深入讲解,同时通过对源码的分析做进一步了解。
简单介绍
JDK1.7—》哈希表,链表
JDK1.8—》哈希表,链表,红黑树— JDK1.8之后,当链表长度超过8使用红黑树。
非线程安全
0.75的负载因子,扩容必须为原来的两倍。
默认大小为16,传入的初始大小必须为2的幂次方的值,如果不为也会变为2的幂次方的值。
根据HashCode存储数据。
JDK1.8-HashMap数据结构—》哈希表,链表,红黑树
JDK1.8-HashMap源码分析
在jdk1.8中对HashMap进行了优化,在发生hash碰撞,不再采用头插法方式,而是直接插入链表尾部,因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全。
JDK1.8-HashMap构造器
这里的this.threshold本应该指的是HashMap的下次扩容的阈值,仔细看你会发现这里并没有对组成HashMap的数组按你写的大小进行初始化,而是把你的参数赋值给下次的扩容的阈值。
public HashMap(int initialCapacity, float loadFactor) {
//如果初始容量小0 则报错
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果初始容量 大于冗余的最大容量 2的30次幂,
//则改变初始容量为允许的最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//如果传入的负载因子小于等于 0 或者 负载因子为空,则报错
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;//赋值传入的负载因子
this.threshold = tableSizeFor(initialCapacity);
}
/**
这个函数是用来对你申请的容量进行处理让他变成最接近你申请的容量的2次幂的大小,
这里注意:假如你申请的容量为0,最后处理的结果会变成1,代表着你最小的容量为1
**/
static final int tableSizeFor(int cap) {
int n = cap - 1;//n 为初始化容量 - 1
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
/**
如果(初始化容量-1)小于0,则初始化容量为1
如果 (初始化容量-1)的值大于 允许的最大容量,则把容量设置为允许的最大容量
否则 设置为 ((初始化容量-1) + 1)
**/
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
JDK1.8-HashMap的put操作
Put操作流程图:
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 = table 值为当前哈希表的值
* n = tab.length 值为当前哈希表长度
* 如果当前哈希表为空 或者 当前哈希表长度为0
* 则tab = resize
* n = resize.length;
*/
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;<br> //没有hash碰撞时,后续值直接覆盖
/**
* i = (n - 1) & hash 得到的值为当前hash应该插入的数组位置
* p = tab[i]; 把p 指向哈希表下标为i的位置
* 如果该位置为空 ,代表该哈希位置还未插入过数据,
* 则把当前要插入的数据生成新Node直接插入
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//如果哈希表下标为i的的位置有数据则执行以下操作
Node<K,V> e; K k;
/**
* 判断一个两个node是否相同,有两个指标 1.两个node的hash值相同;2.两个node的key相同
* 注意:当前p指向哈希表中下标为i的位置的首位
* 如果首位的哈希值与要新插入的哈希值相同 并且
* k = p.key
* (k == key || (key != null && key.equeals(k);
* 其实意思就是如果要新插入的node的与当前p指向的位置为同一个元素
* 则 e = p;
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
/**
* 注意:当前p指向哈希表中下标为i的位置的首位
* 如果当前p指向的位置的类型已经是红黑树
* 则把新node数据直接插入红黑树中
*/
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
/**
* 注意:当前p指向哈希表中下标为i的位置的首位
* 否则当前p指向的哈希表中下标为i的位置是一个线性链表
*/
else {
for (int binCount = 0; ; ++binCount) {
/**
* 注意:当前p指向哈希表中下标为i的位置的首位
* 循环执行 e = p.next ; 直到 e == null 其实就是循环访问线性链表直到线性链表结尾
* 把要插入的值生成新Node插入线性链表结尾
*/
if ((e = p.next) == null) {
//把要插入的值生成新Node插入线性链表结尾
p.next = newNode(hash, key, value, null);
//如果操作的长度大于等于(8 - 1) 则转红黑树 TREEIFY_THRESHOLD为转红黑树的门槛因子
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);//把当前线性链表转为红黑树
break;//插入新数据后跳出for循环
}
/**
* 循环访问线性链表的过程中对每一个node元素与要插入的元素进行判断
* 判断一个两个node是否相同,有两个指标 1.两个node的hash值相同;2.两个node的key相同
* 如果 为同一个元素则跳出for循环
*/
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//如果未到达线性链表末尾且当前线性链表中不存在于要插入的元素相同的node则继续for循环
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//增加修改的次数
if (++size > threshold)//判断当前哈希表长度是否超过负载门槛
resize();//哈希表扩容
afterNodeInsertion(evict);
return null;
}
(一).每个put操作都有可能会触发哈希表扩容
/**
* JDK1.8---哈希表扩容
* @return
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
/**
* 获取原哈希表容量 如果哈希表为空则容量为0 ,否则为原哈希表长度
*/
int oldCap = (oldTab == null) ? 0 : oldTab.length;
/**
* 获取原哈希表扩容门槛
*/
int oldThr = threshold;
/**
* 初始化新容量和新扩容门槛为0
*/
int newCap, newThr = 0;
/**
//如果原容量大于 0
---这个if语句中计算进行扩容后的容量及新的负载门槛
*/
if (oldCap > 0) {
//判断原容量是否大于等于HashMap允许的容量最大值 2的30次幂
if (oldCap >= MAXIMUM_CAPACITY) {
//如果原容量已经大于等于了允许的最大容量,
// 则把当前HashMap的扩容门槛设置为Integer允许的最大值
threshold = Integer.MAX_VALUE;
return oldTab;//不再扩容直接返回
}
/**
* newCap = oldCap << 1 ; 类似 newCap = oldCap * 2 移位操作更加高效
* 表示把原容量的二进制位向左移动一位,
* 扩大为原来的2倍,同样还是2的n次幂
* 如果新的数组容量小于HashMap允许的容量最大值 2的30次幂
* 并且 原数组容量小于默认的初始化数组容量 2的4次幂 =16
*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
/**
* //新的扩容门槛为原来的扩容门槛的2倍,同样二进制左移操作
//类似 newThr = oldThr * 2 移位操作更加高效
*/
newThr = oldThr << 1; // double threshold
}
/**
* 如果 原数组容量小于等于零
* 并且 原负载门槛大于0 则
* 新数组容量为原负载门槛大小
*/
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
/**
* 这个elese语句 初始化默认容量和默认负载门槛
* 如果原数组容量小于等于0
* 并且原负载门槛也小于等于0
* 则
* 新 数组容量为 默认HashMap设置的默认初始化容量 1《4 = 2的4次幂 = 16
* 新 负载门槛为 默认负载因子(0.75f) * 16;
*/
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/**
* 如果新负载门槛为 0 则开始使用新的 数组容量进行计算
*/
if (newThr == 0) {
// 新的数组容量 * 负载因子
float ft = (float)newCap * loadFactor;
/**
* 如果新数组容量 小于 HashMap允许的最大容量(2的30次幂)
* 并且 新计算的负载门槛 小于 HashMap允许的最大容量(2的30次幂)
* 则新的 负载门槛为 计算后的值 的最大整型 -直接截取
* 否则 新的负载门槛为Integer.MAX_VALUE
*/
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//设置全局负载门槛为计算后的新的负载门槛
threshold = newThr;
/**
* 根据新的数组容量创建新的哈希桶 赋值给newTab
*/
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
/**
* 把新创建的哈希桶赋值给全局table;
*/
table = newTab;
/**
* 现在开始真正的扩容
*/
if (oldTab != null) {//如果老的哈希表不为空则执行以下语句
//for 循环,循环老的容量次
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
/**
* //从哈希数组的第一个下标(0)开始开始递增
* 注释:
* e = oldTab[0] ;
* e = oldTab[1] ; 循环访问每次哈希数组下标的内容
* e = oldTab[j];
* 如果 e != null 则开始访问数组中的内容
*/
if ((e = oldTab[j]) != null) {
把原数组中下标为j的位置置空
oldTab[j] = null;
//e.next == null 则代表线性链表只有一个元素e
if (e.next == null)
/**
* //根据e 的哈希值和 (新数组容量 -1)相与得到 e该存放到新数组中的下标
* 然后把e放入对应新数组的下标中。
*/
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
/**
* //如果当前e的类型已经改变为红黑树
* 则对红黑树进行分割 ?
*/
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
/**
* 进入这个else循环代表当前数组下标中存放的元素还是线性链表
*/
Node<K,V> loHead = null, loTail = null;//定义两个指针,分别指向低位头部和低位尾部
Node<K,V> hiHead = null, hiTail = null;//定义两个指针,分别指向高位头部和高位尾部
Node<K,V> next;
/**
* do-while循环中针对数组下标为j的 线性链表进行循环查询,直到线性链表结束
* 并根据每个Node的hash值与原数组容量相与得到新的值。
* 与原数组容量相与后的值只会为0 或 原数组容量。
* 根据得到的这两个值 进行判断
* 如果 值为 0 则把他们放到 loHead和loTail指向的新的线性链表当中--尾部插入
* 如果 值为 原数组容量 则把他们放到 hiHead和hiTail指向的新的线性链表当中--尾部插入
*/
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);
/**
* 原线性链表结束
* 如果新的loTail指向的线性链表不为空,则把它的最后结尾值的指针指向null值
* 并把loHeah与loTail指向的新的链表放到新数组下标为j的位置上。
* 如果新的hiTail指向的线性链表不为空,则把它的最后结尾值的指针指向null值
* 并把hiHeah与hiTail指向的新的链表放到新数组下标为 (j + oldCap) 的位置上。
*/
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
我们使用一个例子来表示链表的哈希扩容
总图(后面有分图和详细图):
分图一:
详细图:
至此JDK1.7和JDK1.8中HashMap的构造方法、put操作、扩容等信息都介绍完毕。
后续我们将要针对特定的问题对HashMap进行系统的介绍。
往期文章链接
Java集合
Java-IO体系
一、C10K问题经典问答
二、java.nio.ByteBuffer用法小结
三、Channel 通道
四、Selector选择器
五、Centos-Linux安装nc
六、windows环境下netcat的安装及使用
七、IDEA的maven项目的netty包的导入(其他jar同)
八、JAVA IO/NIO
九、网络IO原理-创建ServerSocket的过程
十、网络IO原理-彻底弄懂IO
十一、JAVA中ServerSocket调用Linux系统内核
十二、IO进化过程之BIO
十三、Java-IO进化过程之NIO
十四、使用Selector(多路复用器)实现Netty中Reactor单线程模型
十五、使用Selector(多路复用器)实现Netty中Reactor主从模型
十六、Netty入门服务端代码
如需了解更多更详细内容也可关注本人CSDN博客:不吃_花椒
Java集合还需要学习的内容