HashMap 详解三
resize 方法
数组为空或者元素数量超过阈值, 将会执行 resize()
方法, 结果是将数组的长度加倍.
final Node<K,V>[] resize() {
// 设置旧数组, 旧长度, 旧阈值
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
// 初始新长度 新阈值为 0
int newCap, newThr = 0;
// 对原来已经有元素的数组进行处理
if (oldCap > 0) {
// 元素数目超出最大范围, 把阈值调整到最大值, 同时返回原数组
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 对于长度没有超过最大范围, 长度加倍, 阈值也加倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 旧数组长度为 0, 但是旧阈值不为 0, 那么新阈值还是等于旧阈值
// 这种是在指定初始长度构造 HashMap 的情况, 可以通过上一 part 了解下
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 对于旧数组长度为 0, 阈值也为 0 的情况,
// 初始化新长度为默认长度 16,
// 新阈值为`默认长度*默认加载因子` 16*0.75 = 12
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 只有指定初始长度构造 HashMap 才会出现新阈值为 0 这一情况,
// 同样通过长度*加载因子计算出新阈值
if (newThr == 0) {
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 = newTab;
// 把旧数组的元素复制到新数组, 对于元素处理有三种情况
// 1. 只有一个元素, 没有形成链表或者树, 直接把元素放入新数组
// 2. 该元素是树节点, 那么按照树的方式处理
// 3. 如果是链表结构, 那么需要遍历链表
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
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 { // preserve order
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;
}
复制旧链表到新数组
由于元素所在索引值是通过哈希值和数组长度减一进行与操作来得到, 当数组扩容一倍后, 元素在新数组的索引值就有两种情况, 一种是跟旧数组索引值一样, 另一种是旧数组索引值加旧数组长度.
举个例子, 因为数组长度一直都是 2 次幂, 那么假设旧数组长度是 10000, 减一是 1111, 新数组长度 100000, 减一就是 11111. 元素在旧数组的索引值是 xxxx, 在新数组的索引值便是 0xxxx 或者 1xxxx = xxxx + 10000(旧数组长度), 因此可以直接通过旧索引值+旧数组长度来得出新索引值.
那么现在的问题就是如何判断元素是在原来的索引位置还是新的索引位置, 其实就是判断哈希值的高位是 0 还是 1, 这里可以通过元素的哈希值和旧数组长度进行与运算得出, 如果结果是 0, 对应的就是原来位置, 非 0 对应新位置.
- 源代码用头尾节点来记录链表, 因为要生成两个链表, 所以有两对头尾节点, 如下:
// 旧索引位置对应的头尾节点
Node<K,V> loHead = null, loTail = null;
// 新索引位置对应的头尾节点
Node<K,V> hiHead = null, hiTail = null;
// 记录下一个节点
Node<K,V> next;
- 开始遍历旧数组链表, 源代码用了 do...while() 方式循环遍历, 如下:
do {
next = e.next;
...
} while ((e = next) != null)
- 判断该元素是放到旧索引位置还是新索引位置, 判断方法是上面所说的哈希值和旧数组长度做与运算, 如下:
// 与运算为 0, 说明是旧索引位置
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;
}
- 当一个链表遍历完成了, 我们将得到两个新链表, 把这两个新链表保存到数组就完成了, 如下:
// 尾节点不为空, 链表是存在的, 把头节点保存到新数组的旧索引值
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 把头节点保存到新数组的新索引值
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
星云精准测试之用例魔方
精准测试从某个层面来讲,是赋予了测试用例真正的生命力,传统的测试用例仅仅是一些只能够依赖人去理解和分析的文本文件而已,在计算机和算法层面则没有存在意义和价值。下图是精准测试的整体架构图: 大家首先可能会比较好奇,“用例魔方”的概念是怎么来的?测试用例魔方是在精准测试的设计、开发和商业实践中自然产生的功能集合的一个统称。当我们把精准测试的和用例分析相关的功能画成架构图形表示的时候,它自然而然地看起来就像魔方,所谓“魔”则是精准测试核心算法所赋予的超能力。上图是星云精准测试系统的总体结构图,“测试魔方”即分布在左上角区域。大家知道精准测试的核心技术是测试用例与代码的追溯关系的建立,而在此之上就可以构建测试魔方的核心功能区。如下: 所谓“方”实际上是代表测试用例的集合,每个测试用例用一个小方块标识,所有测试用例的集合用一个大方块。现在来看在精准测试架构下,“用例魔方”所能够提供的功能(对精准测试的底层技术不是很了解的话,可以预先温习下《精准测试框架白皮书》)。精准测试体系中,测试用例对应的代码逻辑都可以实现全自动的追溯和存储,因此测试用例就具备了进行深入分析的基础。在精准测试的用例魔方中...
-
下一篇
关于Java序列化你不知道的事
关于本系列 您觉得自己懂 Java 编程?事实上,大多数程序员对于 Java 平台都是浅尝则止,只学习了足以完成手头上任务的知识而已。在本系列 中,Ted Neward 深入挖掘 Java 平台的核心功能,揭示一些鲜为人知的事实,帮助您解决最棘手的编程挑战。 大约一年前,一个负责管理应用程序所有用户设置的开发人员,决定将用户设置存储在一个 Hashtable中,然后将这个 Hashtable 序列化到磁盘,以便持久化。当用户更改设置时,便重新将 Hashtable 写到磁盘。 这是一个优雅的、开放式的设置系统,但是,当团队决定从 Hashtable 迁移到 Java Collections 库中的HashMap 时,这个系统便面临崩溃。 Hashtable 和 HashMap 在磁盘上的格式是不相同、不兼容的。除非对每个持久化的用户设置运行某种类型的数据转换实用程序(极其庞大的任务),否则以后似乎只能一直用Hashtable 作为应用程序的存储格式。 团队感到陷入僵局,但这只是因为他们不知道关于 Java 序列化的一个重要事实:Java 序列化允许随着时间的推移而改变类型。当我向他们展...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS8编译安装MySQL8.0.19
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Dcoker安装(在线仓库),最新的服务器搭配容器使用
- CentOS关闭SELinux安全模块
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果