HashMap1.8源码解析

首先看一下HashMap1.8的继承关系

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {}

和1.7一样1.8不仅继承了AbstractMap,而且实现了Map、Cloneable和Serializable接口,所以HashMap也可以序列化。

HashMap1.8的存储结构
在1.7中,HashMap是以“数组+链表”的基本结构来存储key和value构成的Entry单元的。其中链表结构的存在是用来处理hash碰撞的。这种结构有它的优点,比如容易实现等。但是我们可以设想这样一种情况,如果说有成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那将不可避免的花费o(n)o(n)的时间复杂度来进行查找。基于这点,1.8中将HashMap的基本结构进行了改善,其中hashMap的基本结构依然是“数组+链表”,但是当hash碰撞太多以至于链表过长的时候,链表结构将演化成树(具体来说应该是红黑树)的结构。我们都知道,红黑树是二叉查找树平衡形式的一种,因此查找性能较链表来说,有了很大提升。
在1.7中,是使用Entry这个类作为基本存储单元的,在1.8中,可能为了配合红黑树的使用,改进成了Node这个类,当然,差不多只是名字变了而已,类内部实现的形式差别不是很大。

可以通过下面这张图理解:

HashMap1.8的成员属性

    private static final long serialVersionUID = 362498820763181265L;static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16,最小容量:16static final int MAXIMUM_CAPACITY = 1 << 30;//HashMap的最大容量static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认的负载因子,这里和1.7中的原因是一样的,都是为了在时间和空间上做一个折中,选择最合适的负载因子以保证最优化//树的门阀值,即当链表的长度超过这个值的时候,进行链表到树结构的转变static final int TREEIFY_THRESHOLD = 8;//当低于这个值时,树变成链表static final int UNTREEIFY_THRESHOLD = 6;//下面这个值的意义是:位桶(bin)处的数据要采用红黑树结构进行存储时,整个Table的最小容量static final int MIN_TREEIFY_CAPACITY = 64;//分配的时候,table的长度总是2的幂transient Node<K,V>[] table;transient Set<Map.Entry<K,V>> entrySet;//总的KV数量transient int size;//这个值用于快速失败机制transient int modCount;//门限阈值,计算方法:容量*负载因子int threshold;

HashMap1.8的构造方法

 //  初始容量和加载因子public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)//   如果传入的数组的大小小于0,抛出异常throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);//    如果传入的数组的大小最大值就是MAXIMUM_CAPACITYif (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;// 加载因子比0小,或者加载因子不是一个number,抛出异常if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}//  只传输一个数组的大小,加载因子是默认的0.75public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}//   无参构造,加载因子是默认的0.75public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}//  传递一个Map类型的m,加载因子还是默认的0.75public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}

HashMap的put方法

  public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}

put方法直接调用了putVal方法

 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;//  现在的tab表示Node数组,n表示其长度if ((p = tab[i = (n - 1) & hash]) == null)//  执行到这里,表明当前的索引到的index下标,还未存放元素tab[i] = newNode(hash, key, value, null);else {//  执行到这里,表明当前index下标已经存放了元素Node<K,V> e; K k;//  检测要放的元素的key和已经存放在index下标的元素的key是否相等if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))//  这里表明两个key是相等的,那么进行覆盖,替换原来的旧值e = p;else if (p instanceof TreeNode) //  若两个key不相等,那么检测当前下标所形成的非线性结构是否是红黑树?//  执行到这里,表明非线性结构是红黑树//  那就把它插入到红黑树里面e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {//  执行到这里,表明存储结构是链表//  那就先对链表进行遍历,检测是否存过在这个keyfor (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {//  执行到这里,表明已经遍历到了末尾,那就直接将这个新节点放入到链表的末尾p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st    //  这里进行判断,是否当前链表的长度已经 >= 8//  执行到这里,表明已经 >= 8,那么将这个链表转化成红黑树进行存储// 当然,我们前面提到,转化成红黑树需要两个条件,这里只满足了其中之一//    第二个条件在treeifyBin()函数里面进行判断treeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))  // 这里判断是否存在key相等的节点//    相等,跳出循环break;p = e;}}//  由于e!=null,那么就说明存在key,if (e != null) { // existing mapping for key//  这里进行旧值的替换V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;// 开始执行子类覆盖节点之后的方法afterNodeAccess(e);//    因为是覆盖,所以长度并未增加,可以直接返回return oldValue;}}//  这个modCount是作为迭代器的总长度来用,++modCount;//  先对hashmap的总容量进行+1,然后比较它和阈值的大小if (++size > threshold)//  已经比阈值大,那么进行扩容resize();afterNodeInsertion(evict);return null;}

在putVal方法中有这样一条判断:

if ((p = tab[i = (n - 1) & hash]) == null)

实际上是hash值对(数字长度-1)取余(这里用位运算是因为位运算效率高)。
table的下标是通过将(数组长度-1)与hash值进行与运算得到的。在这个下标对应的位置进行存储。
得到这个下标,进行了一系列操作。目的是为了将它进行位扰动,从而增加散列度,减少哈希碰撞。

进行插入操作,分为三种情况:
1.插入位置无数据,直接存入
2.插入位置有数据,但是较少且符合链表结构存储的条件,那么以链表操作存入
3.插入位置有数据,但是以树结构进行存储,那么以树的相关操作进行存入
较1.7的put相比,复杂了很多,不过却换取了查找时的性能提升。

HashMap真正的初始化还是在put方法中进行的resize操作。

resize()方法

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;//  判断table是否已经初始化过if (oldCap > 0) {//  oldCap > 0,说明table已经初始化过了//  判断旧的数组的容量是否已经达到或多于默认的最大容量(1 << 30)if (oldCap >= MAXIMUM_CAPACITY) {//  改变阈值为整型量的最大值:threshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)//  左移1位,表示扩大到原来的两倍newThr = oldThr << 1; // double threshold}//  判断旧的数组的阈值是否大于0else if (oldThr > 0) // initial capacity was placed in threshold//  新的容量设置为阈值newCap = oldThr;//  数组从未初始化过else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 判断上述操作后新的最佳容量是否计算,若没有,就利用负载因子和新的总容量计算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;//   从这里开始,是将旧的数组下的链,移到新的数组中去if (oldTab != null) {//  开始循环遍历原来的数组for (int j = 0; j < oldCap; ++j) {Node<K,V> e;// 如果当前的数组元素不为null,把值赋值给eif ((e = oldTab[j]) != null) {// 把当前的数组元素赋值为nulloldTab[j] = null;if (e.next == null)//    表明数组元素没后后继节点,该桶中只有一个节点// 将该节点放到新数组中(下标通过hash运算和长度-1相与得到)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;// 判断当前的bin在新数组中是否改变了位置if ((e.hash & oldCap) == 0) {//   结果为0,表明在新数组中的位置没有变动if (loTail == null)//   当前的低位的头指向e所指向的空间,也就是链表的头部loHead = e;else//  当前低位的尾的next指向e所指向的空间loTail.next = e;// 当前的低位的尾指向了e所指向的空间loTail = e;// 上述这几条语句,目的就是为了让头指针指向链表的头部,尾指针一直指向e所指向的空间}else {// 这个else里面的语句的意思与上面if里面的意思一样,只不过是这个是存放在数组中下标高的链if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);//    这个do-while循环,目的就是进行链的遍历,并自行判断应该放在原来的位置还是新的位置。//   然后下面这些语句是将这条链放在新的数组中,if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}//    执行完一次,继续循环执行,直到循环完旧的数组}}//    将新生成的数组进行返回,这也是等于是将旧的数组进行了扩容return newTab;}

树化操作treefyBin:

//对链表进行树结构的转化存储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)resize();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);}}

关于HashMap中的tableSizeFor方法

 //返回根据给定的目标容量所计算出来的最接近的2的幂,这有利于改善hash算法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;}

总结
1.8的HashMap源码较多,这里挑了一些讲下,其余的还是很好理解的。

  • 1.8中HashMap的基本结构还是以数组+链表的形式来存储的,链表没有达到树化的最小数量MIN_TREEIFY_CAPACITY,则进行扩容操作。满足树化的条件,则把链表的每个节点都转化为 TreeNode。通过TreeNode的treeify(Node<K,V>[] tab)方法构建树。
  • 源码要求HashMap底层实现数组的长度为2的幂,原因是可以得到较好的散列性能。

HashMap1.8源码解析相关推荐

  1. 谷歌BERT预训练源码解析(二):模型构建

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/weixin_39470744/arti ...

  2. 谷歌BERT预训练源码解析(三):训练过程

    目录 前言 源码解析 主函数 自定义模型 遮蔽词预测 下一句预测 规范化数据集 前言 本部分介绍BERT训练过程,BERT模型训练过程是在自己的TPU上进行的,这部分我没做过研究所以不做深入探讨.BE ...

  3. 谷歌BERT预训练源码解析(一):训练数据生成

    目录 预训练源码结构简介 输入输出 源码解析 参数 主函数 创建训练实例 下一句预测&实例生成 随机遮蔽 输出 结果一览 预训练源码结构简介 关于BERT,简单来说,它是一个基于Transfo ...

  4. Gin源码解析和例子——中间件(middleware)

    在<Gin源码解析和例子--路由>一文中,我们已经初识中间件.本文将继续探讨这个技术.(转载请指明出于breaksoftware的csdn博客) Gin的中间件,本质是一个匿名回调函数.这 ...

  5. Colly源码解析——结合例子分析底层实现

    通过<Colly源码解析--框架>分析,我们可以知道Colly执行的主要流程.本文将结合http://go-colly.org上的例子分析一些高级设置的底层实现.(转载请指明出于break ...

  6. libev源码解析——定时器监视器和组织形式

    我们先看下定时器监视器的数据结构.(转载请指明出于breaksoftware的csdn博客) /* invoked after a specific time, repeatable (based o ...

  7. libev源码解析——定时器原理

    本文将回答<libev源码解析--I/O模型>中抛出的两个问题.(转载请指明出于breaksoftware的csdn博客) 对于问题1:为什么backend_poll函数需要指定超时?我们 ...

  8. libev源码解析——I/O模型

    在<libev源码解析--总览>一文中,我们介绍过,libev是一个基于事件的循环库.本文将介绍其和事件及循环之间的关系.(转载请指明出于breaksoftware的csdn博客) 目前i ...

  9. libev源码解析——调度策略

    在<libev源码解析--监视器(watcher)结构和组织形式>中介绍过,监视器分为[2,-2]区间5个等级的优先级.等级为2的监视器最高优,然后依次递减.不区分监视器类型和关联的文件描 ...

最新文章

  1. bzoj3467: Crash和陶陶的游戏
  2. 《FlaskWeb开发:基于Python的Web应用开发实战》笔记
  3. openssl-1.0.1e for arm
  4. java web文件上传详解_java web图片上传和文件上传实例详解
  5. 程序员的思维修炼9——超越专家
  6. 怎么虚拟机做成服务器,虚拟机怎么做成服务器
  7. linux ps2鼠标驱动,a1657苹果鼠标驱动 最新版:VoodooPS2Controller v1.9
  8. Java设计模式超详细
  9. 前端面试官经验总结 | 前端面试小技巧
  10. 航空公司客户价值分析(python)
  11. AD 20 PCB 导入CAD图形错乱-问题笔记
  12. vue中虚拟dom和diff算法
  13. 安装已中止,安装程序并未成功地运行完成 - Windows
  14. 听老外吐槽框架设计,Why I Hate Frameworks?
  15. 如何优雅的使用LiveData实现一套EventBus(事件总线)
  16. jQuery学习笔记(3)之Ajax下
  17. 王力宏 有兴趣就看看
  18. 远程智能化灌溉系统 解决方案
  19. win10怎么卸载linux小红帽,win10下使用Linux(ubuntu18.04)
  20. android4.3 adhoc补丁,小wifi教您如何安装安卓系统ADHOC补丁-爱毒霸交流论坛

热门文章

  1. 被 ChatGPT “霍霍”的文学界:由 AI 编写的投稿激增,17 岁老牌杂志宣布暂停征稿...
  2. 【搜索排序】召回综述Semantic Models for the First-Stage Retrieval: A Comprehensive Review
  3. 系统编程概述(进程)
  4. mysql optimize原理_MySQL数据库入门:表的Optimize 优化
  5. 解决无法注入spring容器,获取不到spring容器中的bean问题
  6. 集成网易云信实现自定义消息(类似淘宝聊天发送商品信息)
  7. 七夕节快到了,教你用MATLAB绘制blingbling的大钻石
  8. 仿微信悬浮窗,可缩放悬浮窗,支持自定义展开布局
  9. html微信悬浮窗,微信新功能悬浮窗怎么用
  10. lenpython执行结果_哪个选项是下面代码的执行结果? len ( Python 语言程序设计课程 )_学小易找答案...