当前位置:   article > 正文

HashMap源码分析

HashMap源码分析

一、前言

​ 面试过的人都知道,HashMap是Java程序员在面试中最最最经常被问到的一个点,可以说,不了解HashMap都不好意思说自己是做Java开发的。基本上你去面试十家公司,有七八家都会问到你HashMap。那么今天,就带着大家从源码的角度去分析一下,HashMap具体是怎么实现的。

二、HashMap的构造方法

1.HashMap构造方法

我们先来看HashMap的四个构造方法

  1. //initialCapacity给定的初始化容量,loadFactor扩容因子
  2. public HashMap(int initialCapacity , float loadFactor) {
  3. if (initialCapacity < 0)
  4. throw new IllegalArgumentException("Illegal initial capacity: " +
  5. initialCapacity);
  6. if (initialCapacity > MAXIMUM_CAPACITY)
  7. initialCapacity = MAXIMUM_CAPACITY;
  8. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  9. throw new IllegalArgumentException("Illegal load factor: " +
  10. loadFactor);
  11. this.loadFactor = loadFactor;
  12. this.threshold = tableSizeFor(initialCapacity);
  13. }
  14. public HashMap(int initialCapacity) {
  15. //内部调用了上边的构造方法
  16. this(initialCapacity, DEFAULT_LOAD_FACTOR);
  17. }
  18. public HashMap() {//空参构造
  19. this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
  20. }
  21. public HashMap(Map<? extends K, ? extends V> m) {//构造传入一个map,将map中的值放到hashmap中
  22. this.loadFactor = DEFAULT_LOAD_FACTOR;
  23. putMapEntries(m, false);
  24. }

2.构造方法里的putMapEntries方法

刚才构造方法中提到了putMapEntries这个方法,接下来就让我们一起看一下

  1. //该函数用于将一个map赋值给新的HashMap
  2. final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
  3. //定义变量接收旧hashmap的size
  4. int s = m.size();
  5. //判断s的容量是否大于0
  6. if (s > 0) {
  7. //判断当前数组有没有初始化
  8. if (table == null) { // pre-size
  9. 求出以 旧hashmap数组容量为阈值 的数组容量赋值给ft
  10. float ft = ((float)s / loadFactor) + 1.0F;
  11. //判断是不是大于最大容量,如果是,赋值为最大容量,否则将ft赋值给t
  12. int t = ((ft < (float)MAXIMUM_CAPACITY) ?
  13. (int)ft : MAXIMUM_CAPACITY);
  14. //判断t是否大于threshold(数组扩容阈值)
  15. if (t > threshold)
  16. //通过tablesizefor方法求出大于等于t的最小2的次幂值赋值给threshold(数组扩容阈值)
  17. threshold = tableSizeFor(t);
  18. }
  19. //如果数组长度大于扩容阈值,进行resize扩容操作
  20. else if (s > threshold)
  21. resize();
  22. //循环遍历取出旧hashmap的值放入当前hashmap
  23. for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
  24. K key = e.getKey();
  25. V value = e.getValue();
  26. putVal(hash(key), key, value, false, evict);
  27. }
  28. }
  29. }
  30. /*
  31. if (table == null)分支,是判断当前数组是否初始化,因为在jdk1.8之后,只有当你第一次放值时,才会帮你创建16位的数组。
  32. float ft = ((float)s / loadFactor) + 1.0F经过除运算再加上1.0F是为了向上取整
  33. if (t > threshold)注意一个细节,做判断的的时候,数组还没有初始化,这里的threshold的值还是给定的数组长度的值,也就是capacity的值
  34. else if (s > threshold)说明传入的map集合大于当前的扩容阈值,需要进行resize扩容操作
  35. */

tableSizeFor方

刚才putMapEntries方法中,调用了tableSizeFor方法,接下来我们看一下这个tableSizeFor方法。

作用:创建hashMap对象时调用有参构造,传入初始容量,需要保证hashMap的初始长度为2的n次幂。

tableSizeFor方法用来计算大于等于并离传入值最近的2的n次幂的数值,比如传入15,算出结果为16,传入17,算出结果为32。

通过二进制的位移,第一次右移一位,第二次右移两位,第三次右移四位。。。通过五次位移,将范围内所有的位数都变为1,高位补0,最后得出来的结果再加上1,最后算出来的结果一定是2的n次幂。

  1. //这个方法的作用是找到大于等于给定容量的最小2的次幂值
  2. //>>>表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0
  3. 7--》8
  4. 16--》16
  5. static final int tableSizeFor(int cap) {
  6. //先将数组长度减1,之所以在开始移位前先将容量-1,是为了避免给定容量已经是8,16这样2的幂时,不减一 直接移位会导致得到的结果比预期大。比如预期16得到应该是16,直接移位的话会得到32
  7. int n = cap - 1;
  8. //右移一位,在进行或运算,这个时候最高位和次高位就已经都是1,此时是21
  9. n |= n >>> 1;
  10. //右移两位,在进行或运算,这个时候由上次运算得出的两个1,变成了四个1
  11. n |= n >>> 2;
  12. //右移四位
  13. n |= n >>> 4;
  14. //右移八位
  15. n |= n >>> 8;
  16. //右移十六位,这个时候所有的位数都变为了1
  17. n |= n >>> 16;
  18. //n+1操作,是为了进1,这个时候算出来的数值就一点是 2的n次幂
  19. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  20. }

移位的思想

2的整数幂用二进制表示都是最高有效位为1,其余全是0。

对任意十进制数转换为2的整数幂,结果是这个数本身的最高有效位的前一位变成1,最高有效位以及其后的位都变为0。

核心思想是,先将最高有效位以及其后的位都变为1,最后再+1,就进位到前一位变成1,其后所有的满2变0。所以关键是如何将最高有效位后面都变为1。

先移位,再或运算

右移一位,再或运算,就有两位变为1;
右移两位,再或运算,就有四位变为1,,,
最后右移16位再或运算,保证32位的int类型整数最高有效位之后的位都能变为1.

以传入参数 24 为例:

24转换为二进制是11000,如下图所示:

经过一次位移如下图所示:

经过第二次位移如下图:

经过第三次位移如下图:

因为经过第三次位移运算,所有的位数都变成了1,后续还有第四次和第五次位移,但是已经右移到了最低位所以是无意义操作(针对当前数值24而言,如果数值很大,第四次和第五次操作可以保证所有位数都是1),这个时候所有的位数都是1,所以最后再给它+1,得出来的数值一定是2的n次幂,并且是离传入值最近的2的n次幂的数值。

得出来的数值是11111,在二进制+1,变为100000;将100000转换为十进制,结果 为32.

可以看出,不管传入的数值是多少,我们都能在二进制下将其所有位都转换为1。而且分别经过1,2,4,8,16次转换,不管这个int类型值多大,我们都会将其转换,只是值较小时,可能多做几次无意义操作。

三、HashMap的底层原理

先来看几个重要的参数:

  1. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认数组初始容量
  2. static final int MAXIMUM_CAPACITY = 1 << 30;//数组最大容量
  3. static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子
  4. static final int TREEIFY_THRESHOLD = 8;//树化的阈值
  5. static final int UNTREEIFY_THRESHOLD = 6;//由树退化到链表的阈值
  6. static final int MIN_TREEIFY_CAPACITY = 64;//树化最小数组容量
  7. //node节点,继承了Map.entry,在Entry原有的K,V的基础上追加了hash和next字段
  8. //分别表示key的hash值和下一个节点
  9. static class Node<K,V> implements Map.Entry<K,V> {
  10. final int hash;
  11. final K key;
  12. V value;
  13. Node<K,V> next;
  14. }
  15. //重写了计算hash的方法
  16. //将 Hash 值的高 16 位右移并与原 Hash 值取异或运算(^),混合高 16 位和低 16 位的值
  17. //得到一个更加散列的低 16 位的 Hash 值。
  18. static final int hash(Object key) {
  19. int h;
  20. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  21. }

HashMap在JDK1.8之前的实现方式 数组+链表,

但是在JDK1.8后对HashMap进行了底层优化,改为了由 数组+链表或者数值+红黑树实现,主要的目的是提高查找效率

  1. Jdk8数组+链表或者数组+红黑树实现,当链表中的元素大于等于8 个并且数组长度大于等于64以后, 会将链表转换为红黑树,当红黑树节点 小于 等于6 时又会退化为链表。
  2. 当new HashMap()时,底层没有创建数组,首次调用put()方法示时,会调用resize方法,底层创建长度为16的数组,jdk8底层的数组是:Node[],而非Entry[],用数组容量大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用resize方法将数组扩容到原来的两倍,在做扩容的时候会生成一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能。

默认的负载因子大小为0.75,数组大小为16。也就是说,默认情况下,那么当HashMap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍。

  1. 在我们Java中任何对象都有hashcode,hash算法就是通过hashcode与自己进行向右位移16的异或运算。这样做是为了使高位的16位hash也能参与运算,使运算出来的hash值足够随机,足够分散,还有产生的数组下标足够随机。

map.put(k,v)实现原理

(1)首先将k,v封装到Node对象当中(节点)。 (2)先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。 (3)下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。

HashMap中的put()方法

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

putVal()方法。

  1. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  2. boolean evict) {
  3. Node<K,V>[] tab; Node<K,V> p; int n, i;
  4. //判断数组是否未初始化
  5. if ((tab = table) == null || (n = tab.length) == 0)
  6. //如果未初始化,调用resize方法 进行初始化
  7. n = (tab = resize()).length;
  8. //通过 & 运算求出该数据(key)的数组下标并判断该下标位置是否有数据
  9. if ((p = tab[i = (n - 1) & hash]) == null)
  10. //如果没有,直接将数据放在该下标位置
  11. tab[i] = newNode(hash, key, value, null);
  12. //该数组下标有数据的情况
  13. else {
  14. Node<K,V> e; K k;
  15. //判断该位置数据的key和新来的数据是否一样
  16. if (p.hash == hash &&
  17. ((k = p.key) == key || (key != null && key.equals(k))))
  18. //如果一样,证明为修改操作,该节点的数据赋值给e,后边会用到
  19. e = p;
  20. //判断是不是红黑树
  21. else if (p instanceof TreeNode)
  22. //如果是红黑树的话,进行红黑树的操作
  23. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  24. //新数据和当前数组既不相同,也不是红黑树节点,证明是链表
  25. else {
  26. //遍历链表
  27. for (int binCount = 0; ; ++binCount) {
  28. //判断next节点,如果为空的话,证明遍历到链表尾部了
  29. if ((e = p.next) == null) {
  30. //把新值放入链表尾部
  31. p.next = newNode(hash, key, value, null);
  32. //因为新插入了一条数据,所以判断链表长度是不是大于等于8
  33. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  34. //如果是,进行转换红黑树操作
  35. treeifyBin(tab, hash);
  36. break;
  37. }
  38. //判断链表当中有数据相同的值,如果一样,证明为修改操作
  39. if (e.hash == hash &&
  40. ((k = e.key) == key || (key != null && key.equals(k))))
  41. break;
  42. //把下一个节点赋值为当前节点
  43. p = e;
  44. }
  45. }
  46. //判断e是否为空(e值为修改操作存放原数据的变量)
  47. if (e != null) { // existing mapping for key
  48. //不为空的话证明是修改操作,取出老值
  49. V oldValue = e.value;
  50. //一定会执行 onlyIfAbsent传进来的是false
  51. if (!onlyIfAbsent || oldValue == null)
  52. //将新值赋值当前节点
  53. e.value = value;
  54. afterNodeAccess(e);
  55. //返回老值
  56. return oldValue;
  57. }
  58. }
  59. //计数器,计算当前节点的修改次数
  60. ++modCount;
  61. //当前数组中的数据数量如果大于扩容阈值
  62. if (++size > threshold)
  63. //进行扩容操作
  64. resize();
  65. //空方法
  66. afterNodeInsertion(evict);
  67. //添加操作时 返回空值
  68. return null;
  69. }

map中resize方法

  1. //扩容、初始化数组
  2. final Node<K,V>[] resize() {
  3. Node<K,V>[] oldTab = table;
  4. //如果当前数组为null的时候,把oldCap老数组容量设置为0
  5. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  6. //老的扩容阈值
  7. int oldThr = threshold;
  8. int newCap, newThr = 0;
  9. //判断数组容量是否大于0,大于0说明数组已经初始化
  10. if (oldCap > 0) {
  11. //判断当前数组长度是否大于最大数组长度
  12. if (oldCap >= MAXIMUM_CAPACITY) {
  13. //如果是,将扩容阈值直接设置为int类型的最大数值并直接返回
  14. threshold = Integer.MAX_VALUE;
  15. return oldTab;
  16. }
  17. //如果在最大长度范围内,则需要扩容 OldCap << 1等价于oldCap*2
  18. //运算过后判断是不是最大值并且oldCap需要大于16
  19. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  20. oldCap >= DEFAULT_INITIAL_CAPACITY)
  21. newThr = oldThr << 1; // double threshold 等价于oldThr*2
  22. }
  23. //如果oldCap<0,但是已经初始化了,像把元素删除完之后的情况,那么它的临界值肯定还存在, 如果是首次初始化,它的临界值则为0
  24. else if (oldThr > 0) // initial capacity was placed in threshold
  25. newCap = oldThr;
  26. //数组未初始化的情况,将阈值和扩容因子都设置为默认值
  27. else { // zero initial threshold signifies using defaults
  28. newCap = DEFAULT_INITIAL_CAPACITY;
  29. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  30. }
  31. //初始化容量小于16的时候,扩容阈值是没有赋值的
  32. if (newThr == 0) {
  33. //创建阈值
  34. float ft = (float)newCap * loadFactor;
  35. //判断新容量和新阈值是否大于最大容量
  36. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  37. (int)ft : Integer.MAX_VALUE);
  38. }
  39. //计算出来的阈值赋值
  40. threshold = newThr;
  41. @SuppressWarnings({"rawtypes","unchecked"})
  42. //根据上边计算得出的容量 创建新的数组
  43. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  44. //赋值
  45. table = newTab;
  46. //扩容操作,判断不为空证明不是初始化数组
  47. if (oldTab != null) {
  48. //遍历数组
  49. for (int j = 0; j < oldCap; ++j) {
  50. Node<K,V> e;
  51. //判断当前下标为j的数组如果不为空的话赋值个e,进行下一步操作
  52. if ((e = oldTab[j]) != null) {
  53. //将数组位置置空
  54. oldTab[j] = null;
  55. //判断是否有下个节点
  56. if (e.next == null)
  57. //如果没有,就重新计算在新数组中的下标并放进去
  58. newTab[e.hash & (newCap - 1)] = e;
  59. //有下个节点的情况,并且判断是否已经树化
  60. else if (e instanceof TreeNode)
  61. //进行红黑树的操作
  62. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  63. //有下个节点的情况,并且没有树化(链表形式)
  64. else {
  65. //比如老数组容量是16,那下标就为0-15
  66. //扩容操作*2,容量就变为32,下标为0-31
  67. //低位:0-15,高位16-31
  68. //定义了四个变量
  69. // 低位头 低位尾
  70. Node<K,V> loHead = null, loTail = null;
  71. // 高位头 高位尾
  72. Node<K,V> hiHead = null, hiTail = null;
  73. //下个节点
  74. Node<K,V> next;
  75. //循环遍历
  76. do {
  77. //取出next节点
  78. next = e.next;
  79. //通过 与操作 计算得出结果为0
  80. if ((e.hash & oldCap) == 0) {
  81. //如果低位尾为null,证明当前数组位置为空,没有任何数据
  82. if (loTail == null)
  83. //将e值放入低位头
  84. loHead = e;
  85. //低位尾不为null,证明已经有数据了
  86. else
  87. //将数据放入next节点
  88. loTail.next = e;
  89. //记录低位尾数据
  90. loTail = e;
  91. }
  92. //通过 与操作 计算得出结果不为0
  93. else {
  94. //如果高位尾为null,证明当前数组位置为空,没有任何数据
  95. if (hiTail == null)
  96. //将e值放入高位头
  97. hiHead = e;
  98. //高位尾不为null,证明已经有数据了
  99. else
  100. //将数据放入next节点
  101. hiTail.next = e;
  102. //记录高位尾数据
  103. hiTail = e;
  104. }
  105. //如果e不为空,证明没有到链表尾部,继续执行循环
  106. } while ((e = next) != null);
  107. //低位尾如果记录的有数据,是链表
  108. if (loTail != null) {
  109. //将下一个元素置空
  110. loTail.next = null;
  111. //将低位头放入新数组的原下标位置
  112. newTab[j] = loHead;
  113. }
  114. //高位尾如果记录的有数据,是链表
  115. if (hiTail != null) {
  116. //将下一个元素置空
  117. hiTail.next = null;
  118. //将高位头放入新数组的(原下标+原数组容量)位置
  119. newTab[j + oldCap] = hiHead;
  120. }
  121. }
  122. }
  123. }
  124. }
  125. //返回新的数组对象
  126. return newTab;
  127. }

map.get(k)实现原理

(1)、先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。 (2)、在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着参数K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。

HashMap中的get()方法

  1. public V get(Object key) {
  2. Node<K,V> e;
  3. return (e = getNode(hash(key), key)) == null ? null : e.value;
  4. }

getNode()方法

  1. final Node<K,V> getNode(int hash, Object key) {
  2. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  3. //判断数组不为null并且长度大于0,并且通过hash算出来的数组下标的位置不为空,证明有数据
  4. if ((tab = table) != null && (n = tab.length) > 0 &&
  5. (first = tab[(n - 1) & hash]) != null) {
  6. //判断数组的位置的key的hash和内容是否等同与要查询的数据
  7. if (first.hash == hash && // always check first node
  8. ((k = first.key) == key || (key != null && key.equals(k))))
  9. //相等的话直接返回
  10. return first;
  11. //判断是否有下个节点
  12. if ((e = first.next) != null) {
  13. //判断是否为为红黑树
  14. if (first instanceof TreeNode)
  15. //进行红黑树查询操作
  16. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  17. //链表查询操作
  18. do {
  19. //循环链表,逐一判断
  20. if (e.hash == hash &&
  21. ((k = e.key) == key || (key != null && key.equals(k))))
  22. //发现key的话就返回
  23. return e;
  24. } while ((e = e.next) != null);
  25. }
  26. }
  27. //没有查询到返回null
  28. return null;
  29. }

四、HashMap常见面试题分析

为何HashMap的数组长度一定是2的次幂?

首先,HashMap的初始化的数组长度一定是2的n次的,每次扩容仍是原来的2倍的话,就不会破坏这个规律,每次扩容后,原数据都会进行数据迁移,根据二进制的计算,扩容后数据要么在原来位置,要么在【原来位置+扩容长度】,这样就不需要重新hash,效率上更高。

HashMap中,如果想存入数据,首先它需要根据key的哈希值去定位落入哪个桶中

HashMap的做法,我总结的是,三步:>>>无符号右移、^异或、&与

具体是:拿着key的哈希值,先“>>>”无符号右移16位,然后“^”异或上key的哈希值,得到一个值,再拿着这个值去“&”上数组长度减一

最后得出一个数(如果数组长度是15的话,那这个数就是一个0-15之间的一个数),这个数就是得出的数组脚标位置,也就是存入的桶的位置。

由上边可以知道,定位桶的位置最后需要做一个 “&” 与运算,&完了得出一个数,就是桶的位置

知道了这些以后,再来说为什么HashMap的长度之所以一定是2的次幂?

至少有以下两个原因

1、HashMap的长度是2的次幂的话,可以让数据更散列更均匀的分布,更充分的利用数组的空间

怎么理解呢?下面举例子说一下如果不是2的次幂的数的话假设数组长度是一个奇数,那参与最后的&运算的肯定就是偶数,那偶数的话,它二进制的最后一个低位肯定是0,0做完&运算得到的肯定也是0,那意味着&完后得到的数的最低位一定是0最低位一定是0的话,那说明一定是一个偶数,换句话说就是:&完得到的数一定是一个偶数,所以&完获取到的脚标永远是偶数位,那意味着奇数位的脚标永远都没值,有一半的空间是浪费的奇数说完了,来说一下偶数,假设数组长度是一个偶数,比如6,那参与&运算的就是55的二进制 00000000 00000000 00000000 00000101发现任何一个数&上5,倒数第二低位永远是0,那就意味着&完以后,最起码肯定得不出2或者3(这点刚开始不好理解,但是好好想一下就能明白)意味着第二和第三脚标位肯定不会有值

虽然偶数的话,不会像奇数那么夸张会有一半的脚标位得不到,但是也总会有一些脚标位得不到的。所以不是2的次幂的话,不管是奇数还是偶数,就肯定注定了某些脚标位永远是没有值的,而某些脚标位永远是没有值的,就意味着浪费空间,会让数据散列的不充分,这对HashMap来说绝对是个灾难!

2、HashMap的长度一定是2的次幂,还有另外一个原因,那就是在扩容迁移的时候不需要再重新通过哈希定位新的位置了。扩容后,元素新的位置,要么在原脚标位,要么在原脚标位+扩容长度这么一个位置。

  1. 比如扩容前长度是8,扩容后长度是16
  2. 第一种情况:
  3. 扩容前:
  4. 00000000 00000000 00000000 00000101
  5. &00000000 00000000 00000000 00000111 8-1=7
  6. -------------------------------------
  7. 101 ===== 5 原来脚标位是5
  8. 扩容后:
  9. 00000000 00000000 00000000 00000101
  10. &00000000 00000000 00000000 00001111 16-1=15
  11. -------------------------------------
  12. 101 ===== 5 扩容后脚标位是5(原脚标位)
  13. 第二种情况:
  14. 扩容前:
  15. 00000000 00000000 00000000 00001101
  16. &00000000 00000000 00000000 00000111 8-1=7
  17. -------------------------------------
  18. 101 ===== 5 原来脚标位是5
  19. 扩容后:
  20. 00000000 00000000 00000000 00001101
  21. &00000000 00000000 00000000 00001111 16-1=15
  22. -------------------------------------
  23. 1101 ===== 13 扩容后脚标位是13(原脚标位+扩容长度)

扩容后到底是在原来位置还是在原脚标位+扩容长度的位置,主要是看新扩容最左边一个1对应的上方数字是0还是1如果是0则扩容后在原来位置,如果是1则扩容后在原脚标位+扩容长度的位置HashMap源码里扩容也是这么做的。

总结来说,就是hash&(n-1)这个计算有关。如果不为n不为2的n次方的话,那转换为二进制情况下,n-1就会有某一位为0,那与hash做了&运算后,该位置永远为0,那么计算出来的数组位置就永远会有某个下标的数组位置是空的,也就是这个位置永远不会有值。

JDK1.7中形成的环形链表

线程一:读取到当前的hashmap情况,在准备扩容时,线程二介入

线程二:读取hashmap,进行扩容

线程一:继续执行

此线程先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,所以B->A->B,形成循环。

JDK1.8中HashMap的性能优化

JDK1.8在1.7的基础上对一些操作进行了优化。

不同JDK1.7JDK1.8
存储结构数组+链表数组+链表+红黑树
初始化方式单独函数inflateTable()集成至扩容方法resize()
hash值计算方式扰动处理=9次扰动=4次位运算+5次异或运算扰动处理=2次扰动=1次位运算+1次异或运算
存放数据规则无冲突时,存放数组;冲突时存放链表无冲突时,存放数组;冲突&链表长度<8:c存放单链表;冲突&链表长度 >8:树化并存放红黑树
插入数据方式头插法(将原位置的数据移动到后一位,再插入数据到该位置)尾插法 (直接插入链表尾部/红黑树)
扩容后存储位置的计算方式全部按照原来的方法进行运算(hashCode()->>扰动函数->>(h&length-1))按照扩容后的规则计算(即扩容后位置=原位置或原位置+旧容量)

注意:JDK1.8里转换为红黑树的时候,数组长度必须大于64,如果数组长度小于64,链表长度达到8的话,会进行resize扩容操作。

五、总结

看到这里,我们已经HashMap的源码和实现有了清晰的理解,并且对它的构造方法再到它的一个put数据和get数据的一个源码解析,还有一些它里边比较精妙和重要的一些方法也都探索清楚了,希望这些知识点可以在大家以后的面试中也能够帮助到大家,斩获一个心仪的offer。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/453375
推荐阅读
相关标签
  

闽ICP备14008679号