文章出处:【noblogs-it技术博客网站】的博客:jdk1.8源码分析

  在Java语言中使用的最多的数据结构大概右两种,第一种是数组,比如Array,ArrayList,第二种链表,比如ArrayLinkedList,基于数组的数据结构特点是查找速度很快,时间复杂度为 O(1),但是删除的速度比较慢,因为每次删除元素的时候需要把后面的所有的元素都要相应的往前移动一位,最坏的情况删除第一个元素,时间复杂度为O(n)。基于链表实现的数据结构的特点是删除的速度比较快,但是查找的速度比较慢,每次查找数据的时候都需要从链表头部开始往下遍历,链表查找最坏时间是O(n)。HashMap 就整合和了数组和链表的有点而设计出来的,它的查找速度为 O(1) + O(a),a为链表长度,事实上hashMap的hash算法能够很好的避免了在插入数据的碰撞问题,所以链表的长度基本不会很长,所以hashMap的查找速度还是很快的。一般地,我们平衡一种结构的性能是看平均时间复杂度的,在 jdk1.8以前hashMap在最糟糕的情况下查找的时间复杂度为 O(1) +O(n) ,n 为数据的大小。在jdk1.8时sun公司对hashMap进行了优化,hashMap的存储结构由原来的数组+链接的结构改成 数组+链表+红黑树的形式。时间复杂度由O(1) + O(n) 降为 O(1) + O(logn)。下面的源码都是基于jdk1.8的。

  HashMap中几个重要的参数:

  1、threshold : 数组的大小,默认长度为16,可以在构造函数中指定初始化大小,但是必须是2的n次方,具体原因在下面将会说到。注意:该值是指数组的大小,并不是指HashMap中已经存放了的数据量,存放的数据的大小总是小于等于 threshold * loadFactor。

  2、loadFactor: 负载因子,默认值为0.75。当HashMap中存储的数据大于阈值(threshold * loadFactor)时,threshold会进行翻倍,执行resize方法,对原数组中所有的元素进行一次重新hash计算,根据hash计算得出的下表放在新的数组中。负载因子的设计是为了减少在put操作时发生的碰撞,因为当我们put的数据越来越多的时候,数组中空的位置也会越来越少,那么发生碰撞的概率也随之增大,碰撞的次数越多对性能由一定的影响。一般地我们不需要对这个值进行设置,使用默认值就可以了。

  3、TREEIFY_THRESHOLD:转换红黑树的阈值,默认值为8。即当数组中链表的长度达到这个值之后,链表就是转换成红黑树,以提高性能。

  4、UNTREEIFY_THRESHOLD:红黑树转链表的阈值,默认值为6。

  HashMap结构

  HashMap是以key-value的形式存储数组中,将数据存在Node节点中,每个Node节点存储了一个key,对应的value和指向下一个Node的指针。HashMap的结构为数组+链表(红黑树),链表为单向链表。结构如下:

或:

  HashMap原理:

  HashMap在进行put(key,value)操的时候,我们源码

  1. /**
  2. * Associates the specified value with the specified key in this map.
  3. * If the map previously contained a mapping for the key, the old
  4. * value is replaced.
  5. *
  6. * @param key key with which the specified value is to be associated
  7. * @param value value to be associated with the specified key
  8. * @return the previous value associated with <tt>key</tt>, or
  9. * <tt>null</tt> if there was no mapping for <tt>key</tt>.
  10. * (A <tt>null</tt> return can also indicate that the map
  11. * previously associated <tt>null</tt> with <tt>key</tt>.)
  12. */
  13. public V put(K key, V value) {
  14. return putVal(hash(key), key, value, false, true);
  15. }
  1. /**
  2. * Implements Map.put and related methods
  3. *
  4. * @param hash hash for key
  5. * @param key the key
  6. * @param value the value to put
  7. * @param onlyIfAbsent if true, don't change existing value
  8. * @param evict if false, the table is in creation mode.
  9. * @return previous value, or null if none
  10. */
  11. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  12. boolean evict) {
  13. Node<K,V>[] tab; Node<K,V> p; int n, i;
  14. if ((tab = table) == null || (n = tab.length) == )
  15. n = (tab = resize()).length;
  16. /**
  17. * 通过位与的方式来确定下标位置,判断当前下标位置是否为空,如果为空直接放入到该位置上
  18. * 不为空则通过equals方法来寻找当前位置上面的元素,如果有相同的key,则将覆盖掉,如果没有则将node放置在对应
  19. * 位置上面
  20. */
  21. if ((p = tab[i = (n - ) & hash]) == null)//直接放到数组中
  22. tab[i] = newNode(hash, key, value, null);
  23. else {//当前位置不为空
  24. Node<K,V> e; K k;
  25. if (p.hash == hash &&
  26. ((k = p.key) == key || (key != null && key.equals(k))))//已存在相同的key的数据,将其覆盖
  27. e = p;
  28. else if (p instanceof TreeNode)//当前位置是红黑树,将Node节点放到红黑树中
  29. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  30. else {//为链表的情况
  31. for (int binCount = ; ; ++binCount) {
  32. if ((e = p.next) == null) {
  33. p.next = newNode(hash, key, value, null);
  34. //链表的长度超过转换红黑数的阈值,则将该链表转成红黑树
  35. if (binCount >= TREEIFY_THRESHOLD - ) // -1 for 1st
  36. treeifyBin(tab, hash);
  37. break;
  38. }
  39. if (e.hash == hash &&
  40. ((k = e.key) == key || (key != null && key.equals(k))))//覆盖相同key的node
  41. break;
  42. p = e;
  43. }
  44. }
  45. if (e != null) { // existing mapping for key
  46. V oldValue = e.value;
  47. if (!onlyIfAbsent || oldValue == null)
  48. e.value = value;
  49. afterNodeAccess(e);
  50. return oldValue;
  51. }
  52. }
  53. ++modCount;//快速失败机制
  54. if (++size > threshold)//每次插入数据都要判断一下当前存储的数据是否需要扩容
  55. resize();
  56. afterNodeInsertion(evict);
  57. return null;
  58. }

  当我们当HashMap中put数据的时候,首先会对传进来的key进行hash计算:

  1. /**
  2. * Computes key.hashCode() and spreads (XORs) higher bits of hash
  3. * to lower. Because the table uses power-of-two masking, sets of
  4. * hashes that vary only in bits above the current mask will
  5. * always collide. (Among known examples are sets of Float keys
  6. * holding consecutive whole numbers in small tables.) So we
  7. * apply a transform that spreads the impact of higher bits
  8. * downward. There is a tradeoff between speed, utility, and
  9. * quality of bit-spreading. Because many common sets of hashes
  10. * are already reasonably distributed (so don't benefit from
  11. * spreading), and because we use trees to handle large sets of
  12. * collisions in bins, we just XOR some shifted bits in the
  13. * cheapest possible way to reduce systematic lossage, as well as
  14. * to incorporate impact of the highest bits that would otherwise
  15. * never be used in index calculations because of table bounds.
  16. */
  17. static final int hash(Object key) {
  18. int h;
  19. return (key == null) ? : (h = key.hashCode()) ^ (h >>> );
  20. }

  jdk1.8开始hash的计算比之前的简单一些,就是对key的hashCode的高16位和低16位进行异或运算。这样做的目的是让key的HashCode 的高位也有计算参与运算,这样计算出来的hash值更加均匀,put数据时能够减少碰撞,提供性能。

  第二步根据key计算出来的值获取到对应的下标,这里并不是使用取模的方式来确定,因为取模的方式相对于位与运算来说性能更低下。下标的计算公式为:当前数组的长度减一 按位与 hash值,得到下标,比如当前数组长度为 16,hash值:54707624,则计算如下:(注意:位与的运算规则为,当两个数均为1时结果才为1,否则结果为0)

  从上面的运算结果,可以得到一个规律,能够参与有效运算的位只有与数组长度减一的位的长度,比如 数组长度为16,那么16-1的二进制为 1111,那么不管key的hash值有多大,最终参与运算的只有后4位,根据位与运算规则,运算结果的最大值为 1111,转换成十进制后即数组的长度减一,最小值为 0000,十进制为0,即结果的范围为 0 ~ size - 1,这个取模的结果是一致的。又因为数组的长度总是2的n次方,对应的二进制 为 1,11,111 ,11111等等,这也是为什么每次扩容时都要扩大至原来的两倍的原因。那么,另外一个问题又来了,为什么一定要时2的n次方呢?其他的值可以吗?下面我们来做一个实验:

  1. public static void main(String[] args) {
  2. for (int i = ; i < ; i++) {
  3. System.out.print((i & ) + " ");
  4. }
  5. }
  6.  
  7. 运算结果:

  当我们使用 2 的n次方-1来运算时,每个余数都有可能得到

  1. public static void main(String[] args) {
  2. for (int i = ; i < ; i++) {
  3. System.out.print((i & ) + " ");
  4. }
  5. }
  6. 运算结果:

  当我们使用 非2的n次方运算时,看运算结果可以看到,有些值是不可能得到的,这样数组的某些位置就永远为空,不仅造成空间的浪费,同时也会大大的提高碰撞的概率。根据位与运算规则,很容易想到其中的原因,首先将13转成二进制:1101,在位与运算时,那个 0 位永远不参与运算,如上面的结果一样,2,3,6等数值是没有的。当且仅当二进制数字全为1的时候,才有可能所有的位都能计算,得到的结果才会更加均匀。这个很容易理解,想一下就明白了的。

  第三步,根据生成的index去数组寻找位置,如果该位置为空直接将node放进去,如果不为空则调用equals方法判断key值是否一致,一致的话就替换成新值,否则寻找下个节点,最终在插入链表的时候会判断当前链表长度是否达到了转换成红黑树的条件(默认链表长度达到8时会转)。

  第四步,数据put成功后判断当前存储的数据大小是否超过了 threshold * loadFactor 的值,超过了就会执行resize方法:

  1. /**
  2. * Initializes or doubles table size. If null, allocates in
  3. * accord with initial capacity target held in field threshold.
  4. * Otherwise, because we are using power-of-two expansion, the
  5. * elements from each bin must either stay at same index, or move
  6. * with a power of two offset in the new table.
  7. *
  8. * @return the table
  9. */
  10. final Node<K,V>[] resize() {
  11. Node<K,V>[] oldTab = table;
  12. int oldCap = (oldTab == null) ? : oldTab.length;
  13. int oldThr = threshold;
  14. int newCap, newThr = ;
  15. if (oldCap > ) {
  16. if (oldCap >= MAXIMUM_CAPACITY) {
  17. threshold = Integer.MAX_VALUE;
  18. return oldTab;
  19. }
  20. else if ((newCap = oldCap << ) < MAXIMUM_CAPACITY &&
  21. oldCap >= DEFAULT_INITIAL_CAPACITY)
  22. newThr = oldThr << ; // double threshold
  23. }
  24. else if (oldThr > ) // initial capacity was placed in threshold
  25. newCap = oldThr;
  26. else { // zero initial threshold signifies using defaults
  27. newCap = DEFAULT_INITIAL_CAPACITY;
  28. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  29. }
  30. if (newThr == ) {
  31. float ft = (float)newCap * loadFactor;
  32. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  33. (int)ft : Integer.MAX_VALUE);
  34. }
  35. threshold = newThr;
  36. @SuppressWarnings({"rawtypes","unchecked"})
  37. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  38. table = newTab;
  39. if (oldTab != null) {
  40. for (int j = ; j < oldCap; ++j) {
  41. Node<K,V> e;
  42. if ((e = oldTab[j]) != null) {
  43. oldTab[j] = null;
  44. if (e.next == null)
  45. newTab[e.hash & (newCap - )] = e;
  46. else if (e instanceof TreeNode)
  47. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  48. else { // preserve order
  49. Node<K,V> loHead = null, loTail = null;
  50. Node<K,V> hiHead = null, hiTail = null;
  51. Node<K,V> next;
  52. do {
  53. next = e.next;
  54. if ((e.hash & oldCap) == ) {
  55. if (loTail == null)
  56. loHead = e;
  57. else
  58. loTail.next = e;
  59. loTail = e;
  60. }
  61. else {
  62. if (hiTail == null)
  63. hiHead = e;
  64. else
  65. hiTail.next = e;
  66. hiTail = e;
  67. }
  68. } while ((e = next) != null);
  69. if (loTail != null) {
  70. loTail.next = null;
  71. newTab[j] = loHead;
  72. }
  73. if (hiTail != null) {
  74. hiTail.next = null;
  75. newTab[j + oldCap] = hiHead;
  76. }
  77. }
  78. }
  79. }
  80. }
  81. return newTab;
  82. }

  进行扩容的时候将所有的node节点进行hash计算e.hash & (newCap - 1),这样的结果不是在原来的就是在 当前位置加原来threshold长度的位置。至此整个put操作结束。

  get(Object key)的原理:

  弄懂了put操作之后,其实get就很容易理解了,首先根据传入key找到index,然后再对应的位置上获取就行了。

  最后,我看了HashMap源码之后,自己也手写了一个HashMap,不同之处在于我没有用到红黑树,而是使用二叉树代替,经测试插入1千万条uuid所需时间都差粗多,都是在二十几秒左右。关于二叉树与红黑树的区别可以自行百度,红黑树最主要解决的问题是在极端的情况下二叉树只有一条路径,时间复杂度位O(N),红黑树为了避免这种情况,每次都会自动调节树的深度,将最坏的情况的时间复杂度降低到O(logN)。

  因为完全是手写的,所以可能代码的可读性不是很好,但是基本的功能都能够实现了。如果大家有兴趣的话,可以下载过来看一下,也欢迎大家指出错误或提意见。项目地址: https://github.com/rainple1860/MyCollection

jdk源码阅读笔记-HashMap的更多相关文章

  1. jdk源码阅读笔记-LinkedHashMap

    Map是Java collection framework 中重要的组成部分,特别是HashMap是在我们在日常的开发的过程中使用的最多的一个集合.但是遗憾的是,存放在HashMap中元素都是无序的, ...

  2. jdk源码阅读笔记-HashSet

    通过阅读源码发现,HashSet底层的实现源码其实就是调用HashMap的方法实现的,所以如果你阅读过HashMap或对HashMap比较熟悉的话,那么阅读HashSet就很轻松,也很容易理解了.我之 ...

  3. jdk源码阅读笔记-ArrayList

    一.ArrayList概述 首先我们来说一下ArrayList是什么?它解决了什么问题?ArrayList其实是一个数组,但是有区别于一般的数组,它是一个可以动态改变大小的动态数组.ArrayList ...

  4. jdk源码阅读笔记

    1.环境搭建 http://www.komorebishao.com/2020/idea-java-jdk-funyard/ 2. 好的源码阅读资源 https://zhuanlan.zhihu.co ...

  5. jdk源码阅读笔记-Integer

    public final class Integer extends Number implements Comparable<Integer> Integer 由final修饰了,所以该 ...

  6. JDK源码学习笔记——HashMap

    Java集合的学习先理清数据结构: 一.属性 //哈希桶,存放链表. 长度是2的N次方,或者初始化时为0. transient Node<K,V>[] table; //最大容量 2的30 ...

  7. jdk源码阅读笔记-LinkedList

    一.LinkedList概述 LinkedList的底层数据结构为双向链表结构,与ArrayList相同的是LinkedList也可以存储相同或null的元素.相对于ArrayList来说,Linke ...

  8. jdk源码阅读笔记-AbstractStringBuilder

    AbstractStringBuilder 在java.lang 包中,是一个抽象类,实现 Appendable 接口和 CharSequence 接口,这个类的诞生是为了解决 String 类在创建 ...

  9. jdk源码阅读笔记-String

    本人自学java两年,有幸初入这个行业,所以功力尚浅,本着学习与交流的态度写一些学习随笔,什么错误的地方,热烈地希望园友们提出来,我们共同进步!这是我入园写的第一篇文章,写得可能会很乱. 一.什么是S ...

随机推荐

  1. GlitchBot -HZNU寒假集训

    One of our delivery robots is malfunctioning! The job of the robot is simple; it should follow a lis ...

  2. Jmeter(二十七)_Beanshell保存响应内容到本地

    利用Jmeter-BeanShell PostProcessor可以提取响应结果并保存到本地文件,这种操作在jmeter做爬虫时非常有用,可以帮助你迅速的获取想要的内容到本地文件! 1:在本地新建一个 ...

  3. lodash中Collection部分所有方法的总结

    总结一下lodash中Collection的所有的方法,方便对比记忆,也便于使用时候查找. 1.    判断是否符合条件:返回bool: a)  every: 判断每一值是不是都符合条件: 通过 pr ...

  4. Windows远程桌面连接 出现身份错误 要求的函数不受支持

    原因 CVE-2018-0886 的 CredSSP 更新 将默认设置从"易受攻击"更改为"缓解"的更新. ## 官方更新 摘要 凭据安全支持提供程序协议 (C ...

  5. Vue.js与Jquery的比较 谁与争锋 js风暴

    普遍认为jQuery是适合web初学者的起步工具.许多人甚至在学习jQuery之前,他们已经学习了一些轻量JavaScript知识.为什么?部分是因为jQuery的流行,但主要是源于经验开发人员的一个 ...

  6. C# 使用SmtpClient发送Email

    使用Winfrom写的报错信息发送邮件通知. 以下主要代码 /// <summary> /// 发送邮件核心代码 /// </summary> /// <param na ...

  7. AUTOSAR分层-MCAL辨析

    8. AUTOSAR中MCAL虽然包含各种drvier,但毕竟是AL即抽象层,不应包含architecture和device特定的信息.应该只包含模型定义,不包含实现细节.   AUTOSAR文档中的 ...

  8. tomcat启动报错:Address already in use: JVM_Bind

    tomcat启动时出现Address already in use: JVM_Bind 的原因是因为端口被占用,有可能是因为多次启动tomcat或者启动了多个tomcat,或者是其他应用程序或者服务占 ...

  9. Python学习 Part7:类

    Python学习 Part7:类 1. 作用域和命名空间 命名空间(namespace)就是一个从名称到对象的映射. 命名空间的一些实例:内置名称集(函数,像abs(),和内置异常名称),一个模块中的 ...

  10. 读《图解HTTP》有感-(HTTP报文内的HTTP消息)

    写在前面 HTTP通信包括从客户端到服务端的的请求以及服务端返回客户端的响应 正文 1.什么是HTTP报文?它由什么构成?包含几个部分? 用于HTTP协议交互的信息就是HTTP报文:它是由多行数据构成 ...