文章目录

  • 核心部分
    • 初始化
    • 添加/移除元素
    • 调试HashMap通过链表法解决hash碰撞
    • 扩容
  • 其他

核心部分

JDK版本: 1.8
几个重要的属性说明:
size  map中元素的个数,与map的容量要区分
threshold   触发扩容的阈值,当元素个数达到该值时,触发resize(),threshold = loadFactor*capacity
loadFactor   加载因子,用于hash table
capacity   map的容量
table    node数组,map存储数据的底层结构

初始化

// 测试demo
public static void main(String[] args) {// 1. 无参构造Map<String, String> map = new HashMap<String, String>();//2. 有参构造Map<String, String> map2 = new HashMap<String, String>(8);
}

通过源码可知,HashMap执行构造方法生成实例,实例内部此时并没有node用于存储数据,有参与无参的区别在于使用默认还是自定义数值去定义map属性,如初始化容量,加载因子

// 无参构造,返回一个默认初始化容量为16和加载因子为0.75的空HashMap
public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}// 有参构造
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;// tableSizeFor() 函数保证返回的值始终是2的幂次方,如 0,1->1, 7->8, 11->16this.threshold = tableSizeFor(initialCapacity);
}

添加/移除元素

当向map中put元素时,开始构造map的内部结构node数组,构造的相关参数都使用默认值,如下源码

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {// node:静态内部类,map中真正用于组织和存储数据Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)// 初始化之后,必定执行一次扩容操作(扩容操作见下面)n = (tab = resize()).length;// 通过hash判断该位置是否已存在元素,如果没有则插入新节点,如果有,则进一步判断if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 此处是为了遍历node节点,当发生碰撞的节点的next节点不为空时不能存放待插入的元素,// 此时将next节点的引用指向p,节点向后遍历if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;// onlyIfAbsent 仅允许不存在的key(仅允许缺席),默认false,即允许重复keyif (!onlyIfAbsent || oldValue == null)// 覆盖旧值e.value = value;afterNodeAccess(e);// 返回旧值return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}

总结下put的流程:

put相同key时,hashMap的处理细节:

调试HashMap通过链表法解决hash碰撞

这里如果想测试链表与红黑树转换的过程,该怎么办?
按照源码的逻辑,只有放入key不同但散列之后hash值相同的元素,即找几个发生hash碰撞的元素作为key放入hashMap,但是一般hash碰撞的概率很小怎么办? 考虑将map中key的泛型定义为自定义对象,并重新自定义对象的hashCode方法,代码如下:

// 自定义对象
public class Doge {String name;public Doge(String name) {this.name = name;}@Overridepublic int hashCode() {// 返回固定值,必定发生碰撞return 8;}
}// 测试代码Map<Doge, String> map = new HashMap<Doge, String>();// 放入元素且每个元素的key都发生hash碰撞map.put(new Doge("00"),"00");map.put(new Doge("11"),"11");map.put(new Doge("22"),"22");map.put(new Doge("33"),"33");map.put(new Doge("44"),"44");map.put(new Doge("55"),"55");map.put(new Doge("66"),"66");map.put(new Doge("77"),"77");map.put(new Doge("88"),"88");

上述测试代码执行的过程如下:

  1. Doge(00)放入map时,执行resize(),且Node数组中hash对应的索引位置没有值,直接newNode()

  2. Doge(11)放入map时,索引位置有值且发生碰撞,且不是TreeNode(静态内部类Node),执行红框部分,与猜测的执行流程一致

  3. Doge(22)放入时,步骤类似.不同的是由于通过hash作为索引定位到的数组元素始终是00(第一次放入),所以此时p为00,p.next为11不为null,需要将p指向11节点,进而完成下个节点的赋值

  4. Doge(33) ~ Doge(77)放入的过程与步骤3一样,就是不断的向节点链表末尾插入

  5. Doge(88)放入时,即链表的7号索引被插入(从0开始,链长度为8),达到阈值触发treeifyBin(tab, hash);这种情况就是因为碰撞次数太多,源码中给出两种方案:
    a. 对节点数组进行扩容,通过增加空间来减少hash碰撞
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(),数组容量阈值64.数组容量不足64且链长度达到8时,优先进行扩容.

    b. 将链表转为红黑树

  6. 根据我们的测试代码,容量默认16,一定会走扩容逻辑,如果想执行红黑树转换逻辑,修改测试代码,初始容量设置为64
    Map<Doge, String> map = new HashMap<Doge, String>(64);

  7. 链表转红黑树简述
    a. 通过replacementTreeNode(e, null);将Node数组中的Node类型节点全部转为TreeNode类型树节点
    b. hd.treeify(tab);,将这些树节点组织成树并与Node数组中的对应索引位置的元素关联

注意:

  1. map中数组和链表都是由Node元素构成,Noke<K,V>[] tab,Node<K,V> p,e,链表结构是通过Node类中的next属性指向下一个节点来实现的

扩容

扩容条件: 大于阈值时(阈值=caploadFactor,默认为12 = 160.75)
扩容的规则描述:

final Node<K,V>[] resize() {// 扩容前的nodeNode<K,V>[] oldTab = table;// 扩容前的容量int oldCap = (oldTab == null) ? 0 : oldTab.length;// 扩容前的阈值int oldThr = threshold;// 新的容量和阈值,默认是16 和 12int 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}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaults// 无参构造首次resize,使用默认配置newCap = 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数组Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;// 首次扩容时,oldTab为null,直接返回table,即新的newTabif (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 orderNode<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;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;
}

总结下扩容流程:

其他

  1. 负载因子为什么是0.75?
    从源码注释可以看到,根据泊松分布,当因子在0.7~0.8之间时,碰撞的几率为百万分之一
  1. 为什么容量必须是2的幂次方
    为了在通过hash计算索引时,索引分布更均匀.
if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);
// 可以看出,索引 i= (n - 1) & hash;
// 当n为2的幂次方时,n-1的2进制表示全为1,此时与hash与运算时,结果只取决于hash值.
// 只要hash值是均匀分布的,那么计算出来的i索引就是均匀分布的.

JDK源码阅读--HashMap相关推荐

  1. jdk源码阅读-HashMap

    前置阅读: jdk源码阅读-Map : http://www.cnblogs.com/ccode/p/4645683.html 在前置阅读的文章里,已经提到HashMap是基于Hash表实现的,所以在 ...

  2. 【JDK】JDK源码分析-HashMap(1)

    概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是"链表法",并且在 J ...

  3. jdk javac运行不了_Intellij IDEA搭建jdk源码阅读环境

    一.找到源码位置 直接找到jdk安装的目录,会看到src.zip的压缩包,这里面就是jdk的源码,例如下图: 在这里解压. 第一次尝试建议使用9或更早版本jdk的源码,否则易造成卡死. 二.Intel ...

  4. Mac搭建JDK源码阅读环境

    点赞再看,养成习惯,微信公众号搜索[虚竹讲程序经],获取更多技术干货! 想要读懂JDK源码,需要在自己电脑上搭建JDK的源码阅读环境,正所谓,工欲善其事,必先利其器.下面演示如何在Mac上结合Idea ...

  5. jdk源码(jdk源码阅读顺序)

    如何在myEclipse中查看JDK源码 myeclipse中查看jdk类库的源码步骤如下: 1.点 "window"-> "Preferences" - ...

  6. JDK源码阅读环境搭建

    内容来源 B站Up主: CodeSheep 视频: https://www.bilibili.com/video/BV1V7411U78L 感谢大佬分享学习心得 Thanks♪(・ω・)ノ~~~ 1. ...

  7. JDK源码阅读-搭建阅读环境

    1.找到源码位置 其实我们安装jdk的时候源码就已经存在,只要找到jdk的安装位置,就能找到源码,如果不知道jdk具体安装位置的话,可以在idea中查看. 打开目录,找到路径下的src.zip,这就是 ...

  8. JDK源码阅读之路【不断更新】

    前言 为什么阅读源码? 学习设计模式和思维.总之,知道自己有多菜,在不断学习的过程中发现自身不足并弥补,才能进步. 如何阅读 阅读顺序参考:https://blog.csdn.net/qq_21033 ...

  9. JDK源码阅读 String

      1.String是如何做到不可变?为什么要将它设计为不可变类?     答:首先String类是被final修饰,不能被继承:它把数据存放在一个数组value中,value同样被final修饰:所 ...

最新文章

  1. 10 年前被删的初恋,凌晨 1 点突然加我…
  2. DEV为什么不能输出小数 浮点数?
  3. 【cocostudio】发布资源在Cocos2d-x中如何使用
  4. php使用redis持久化,Redis持久化完整版本
  5. (42) Aeroo 模板实战
  6. Caffe: gflag编译出现问题汇总
  7. Java回调机制解读
  8. 《JavaScript高级程序设计(第2版)》
  9. python对excel数据统计_数据分析EPHS(4)-使用Excel和Python计算数列统计值
  10. python应用系列教程——python操作office办公软件word
  11. Linux中的readelf命令
  12. 如何用matlab求向量在基下的坐标,请问什么是有关向量的基底、基向量、基坐标?...
  13. Cell Host | 张群业/王哲/张澄-肠道微生物群失调加重腹主动脉瘤
  14. 基于MFC的圆环的消隐实现
  15. 潘多拉STM32L475 1.初探
  16. linux freemind字体,解决freemind中文乱码
  17. 【阿里面试】秋招菜鸟网络一面
  18. 开发中那些难题以及那些哭笑不得的解决过程
  19. golang 批量ping工具
  20. python安装setuptools_python安装setuptools的方法

热门文章

  1. 麻瓜 | 数学建模日记 | 第二天
  2. 阿里影业已完成质变,何时量变?
  3. js对象之hasOwnProperty和in和isPrototypeOf
  4. jquery点击加号多图上传插件
  5. 用尽《会声会影9》六大亮点
  6. bat(批处理文件)教程
  7. Acrobat 9 Pro/AcrobatXIPro/AcrobatDC安装
  8. Android的Google官方设计指南(上)
  9. 我的世界服务器修改mod配置文件,如何更改部分配置文件 - 机械改造 (Cyberware) - MC百科|最大的Minecraft中文MOD百科...
  10. 学设计的有必要考二级计算机吗,大学生有必要考计算机二级吗,如何准备计算机二级考试?...