当前位置:   article > 正文

HashMap实现原理

(first = tab[(n - 1) & hash]) != null)

面试高频问题,这里提供下参考。

img_4d9269ae62a027cc9d365ef726875c7b.png
HashMap结构图

目录

  • 唠叨
  • 解析思路
  • get方法
  • put方法
  • resize方法

唠叨

认真阅读了下HashMap的实现方式,也参考了网上别人的一些解析,个人觉得还是有些东西想说。网上有的文章名字为HashMap源码解析,实际上就是给它里面的一些方法加上一些注释而已,有不少都是这样的。

我自己看源码的时候,发现不是别人不想解析,而是它的实现真的需要亲自研读,多理顺几遍才知道怎么回事。

我在这里解析的文字描述也较多,不管谁的解析,自己也都要看一下JDK源码的具体实现,我们仅提供参考而已。

解析思路

源码不太方便看,先说明一下我的阅读思路。

  1. 把常用的几个方法拷贝到文本编辑器里面。
  2. HashMap中不同的时候会有不同的流程,梳理方法中的逻辑流程。
    就像采用极端法,采用特殊的数据,然后查看方法执行语句。未执行的语句暂时不考虑。
  3. 注释源码...

我觉得HashMap的实现方式不够好,关键的几个方法里面包含的情况太多了,阅读起来是有难度的,而写程序的目的之一不就是让其他开发者阅读吗?一个方法内部做了太多的事情,违反了代码整洁的规则,一个函数做要尽量少的事情。

解析

之前稍微介绍了一些HashMap的特性,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. }
  1. 先从数组下标,找到对应的Node
  2. 如果Node里的第一个节点命中,直接返回
  3. 如果有冲突,则通过key.equals(k)去查找对应的entry
  • 若为树,则在树中通过key.equals(k)查找,O(logn);
  • 若为链表,则在链表中通过key.equals(k)查找,O(n)。
  1. // hash值为hash(key),key
  2. final Node<K,V> getNode(int hash, Object key) {
  3. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  4. //table不为空,并且tab[(n-1) & hash] != null的时候。
  5. if ((tab = table) != null && (n = tab.length) > 0 &&
  6. (first = tab[(n - 1) & hash]) != null) {
  7. //判断取出Node的hash值是否相等。key值相等,那么直接返回。
  8. //想一想什么情况下,if语句不成立?
  9. if (first.hash == hash && // always check first node
  10. ((k = first.key) == key || (key != null && key.equals(k))))
  11. return first;
  12. //也就是取出的第一个Node的hash值与key计算的hash不等。
  13. if ((e = first.next) != null) {
  14. if (first instanceof TreeNode)
  15. //从树中取节点。
  16. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  17. do {
  18. //判断hash值与key值是否相等,一直判断到相等或到节点末端为止。
  19. if (e.hash == hash &&
  20. ((k = e.key) == key || (key != null && key.equals(k))))
  21. return e;
  22. } while ((e = e.next) != null);
  23. }
  24. }
  25. return null;
  26. }
put方法

这个中间涉及的逻辑多一些,方法需要分不同的步骤看。
思路:

  • 对key的hashCode()做hash,然后再计算index;
  • 如果没碰撞直接放到bucket里;
  • 如果碰撞了,以链表的形式存在buckets后;
    • 如果节点已经存在就替换old value(保证key的唯一性)
    • 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
  • 如果Node的容量满了(超过load factor*current capacity),就要resize。
  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }
  4. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  5. boolean evict) {
  6. Node<K,V>[] tab; Node<K,V> p; int n, i;
  7. //如果table为空,就重新创建table
  8. if ((tab = table) == null || (n = tab.length) == 0)
  9. n = (tab = resize()).length;
  10. // 如果tab[(n-1) & hash]为空的话,就在tab[(n-1) & hash]位置存储节点。
  11. // newNode = new Node<>(hash, key, value, next);
  12. if ((p = tab[i = (n - 1) & hash]) == null)
  13. tab[i] = newNode(hash, key, value, null);
  14. else {
  15. //当tab[(n-1)&hash]位置已经存在Node的时候。
  16. Node<K,V> e; K k;
  17. //如果已经存在的Node与即将要存的key值一样
  18. // e为存在的Node
  19. if (p.hash == hash &&
  20. ((k = p.key) == key || (key != null && key.equals(k))))
  21. e = p;
  22. else if (p instanceof TreeNode)
  23. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  24. else {
  25. //不满足以上的情况,一直把Node往后插入。
  26. //如果插入的节点数量多于TREEIFY_THRESHOLD-1个,变为树形节点
  27. for (int binCount = 0; ; ++binCount) {
  28. if ((e = p.next) == null) {
  29. p.next = newNode(hash, key, value, null);
  30. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  31. treeifyBin(tab, hash);
  32. break;
  33. }
  34. //如果再遍历的时候,发现key值相同的时候,就跳出循环。e = p.next
  35. // 这时已经记录e的Node值了
  36. if (e.hash == hash &&
  37. ((k = e.key) == key || (key != null && key.equals(k))))
  38. break;
  39. p = e;
  40. }
  41. }
  42. // 存在对应的Node时
  43. if (e != null) { // existing mapping for key
  44. V oldValue = e.value;
  45. if (!onlyIfAbsent || oldValue == null)
  46. e.value = value;
  47. afterNodeAccess(e);
  48. return oldValue;
  49. }
  50. }
  51. // HashMap内部修改的初始
  52. ++modCount;
  53. //如果存储的节点数,大于临界值,重新分配大小
  54. if (++size > threshold)
  55. resize();
  56. //抽象方法,当节点执行插入操作的时候如何处理
  57. afterNodeInsertion(evict);
  58. return null;
  59. }

一般不发生碰撞的时候,相对简单,数据量较小的情况下。

  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. if ((tab = table) == null || (n = tab.length) == 0)
  5. n = (tab = resize()).length;
  6. // 留意 i = (n-1)&hash,所以取的时候也这样取
  7. // newNode = new Node<>(hash, key, value, next);
  8. if ((p = tab[i = (n - 1) & hash]) == null)
  9. tab[i] = newNode(hash, key, value, null);
  10. ++modCount;
  11. if (++size > threshold)
  12. resize();
  13. afterNodeInsertion(evict);
  14. return null;
  15. }

发生碰撞后,有个红黑树的处理,因为红黑树相对知识点较多,下次单独详细解释。这里可以参考以下,从JDK源码研究红黑树。我解释下关于碰撞冲的循环。

  • 查看是否存在相同的key,存在相同的key跳出循环,覆盖key的value
  • 如果不存在相同的key,在链表末尾插入新的Node
    • 如果链表节点过长,转换为树。
  1. for (int binCount = 0; ; ++binCount) {
  2. // p.nextnull的时候,走到了链表的末端,然后新建一个节点,如果链表的长度太长,转换为树存储。
  3. if ((e = p.next) == null) {
  4. p.next = newNode(hash, key, value, null);
  5. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  6. treeifyBin(tab, hash);
  7. break;
  8. }
  9. // 如果链表中存在于要put的key值相同的时候,存储key值,也就是e ,(e = p.next)。
  10. if (e.hash == hash &&
  11. ((k = e.key) == key || (key != null && key.equals(k))))
  12. break;
  13. p = e;
  14. }

红黑树的部分,我们下次单独解析

resize方法

这个涉及的内容,有不少线需要捋一捋。首先看申明时候会resize()。它们都在调用put的时候执行的。

  • table == null的时候
  1. if ((tab = table) == null || (n = tab.length) == 0)
  2. n = (tab = resize()).length;
  • 键值映射的的数目大于临界值的时候。
  1. if (++size > threshold)
  2. resize();
resize具体方法
  1. final Node<K,V>[] resize() {
  2. Node<K,V>[] oldTab = table;
  3. // 之前的容量可能为0或者为之前的大小
  4. // threshold可能为null或者为2的n次方
  5. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  6. int oldThr = threshold;
  7. //新的容量,新的临界目前都为0
  8. int newCap, newThr = 0;
  9. // 第二次resize的时候。
  10. if (oldCap > 0) {
  11. if (oldCap >= MAXIMUM_CAPACITY) {
  12. threshold = Integer.MAX_VALUE;
  13. return oldTab;
  14. }
  15. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  16. oldCap >= DEFAULT_INITIAL_CAPACITY)
  17. newThr = oldThr << 1; // double threshold
  18. }
  19. //第一次resize()的时候,初始化的操作。
  20. else if (oldThr > 0) // initial capacity was placed in threshold
  21. newCap = oldThr;
  22. else { // zero initial threshold signifies using defaults
  23. newCap = DEFAULT_INITIAL_CAPACITY;
  24. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  25. }
  26. //第一次resize(),会进入
  27. if (newThr == 0) {
  28. //负载因子 * 初始容量
  29. float ft = (float)newCap * loadFactor;
  30. //保证临界值不超过最大值。
  31. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  32. (int)ft : Integer.MAX_VALUE);
  33. }
  34. //真正初始化的操作,新建newCap个数组,临界值初始化。
  35. threshold = newThr;
  36. @SuppressWarnings({"rawtypes","unchecked"})
  37. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  38. table = newTab;
  39. // 非 第一次reizie()时
  40. if (oldTab != null) {
  41. for (int j = 0; j < oldCap; ++j) {
  42. Node<K,V> e;
  43. if ((e = oldTab[j]) != null) {
  44. // 重新计算了一次hash
  45. oldTab[j] = null;
  46. if (e.next == null)
  47. newTab[e.hash & (newCap - 1)] = e;
  48. else if (e instanceof TreeNode)
  49. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  50. // 如果e.next= null 存在hash的Node 链子
  51. else { // preserve order
  52. Node<K,V> loHead = null, loTail = null;
  53. Node<K,V> hiHead = null, hiTail = null;
  54. Node<K,V> next;
  55. do {
  56. next = e.next;
  57. // 原索引
  58. if ((e.hash & oldCap) == 0) {
  59. if (loTail == null)
  60. loHead = e;
  61. else
  62. loTail.next = e;
  63. loTail = e;
  64. }
  65. // 原索引+ oldCap
  66. else {
  67. if (hiTail == null)
  68. hiHead = e;
  69. else
  70. hiTail.next = e;
  71. hiTail = e;
  72. }
  73. } while ((e = next) != null);
  74. if (loTail != null) {
  75. loTail.next = null;
  76. newTab[j] = loHead;
  77. }
  78. if (hiTail != null) {
  79. hiTail.next = null;
  80. newTab[j + oldCap] = hiHead;
  81. }
  82. }
  83. }
  84. }
  85. }
  86. return newTab;
  87. }

如果是第一次resize,我们抽出来会执行到的语句。

  • 初始化容量
  • 初始化threshold,也就是初始化临界值,决定了table的键值对数目到什么时候会再次resize()
  1. final Node<K,V>[] resize() {
  2. //第一次的时候tablenull
  3. Node<K,V>[] oldTab = table;
  4. // oldCap 为 0,threshod为null
  5. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  6. int oldThr = threshold;
  7. int newCap, newThr = 0;
  8. // 不会走
  9. if (oldCap > 0) {
  10. ....
  11. // 从这里执行
  12. else { // zero initial threshold signifies using defaults
  13. newCap = DEFAULT_INITIAL_CAPACITY;
  14. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  15. }
  16. threshold = newThr;
  17. @SuppressWarnings({"rawtypes","unchecked"})
  18. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  19. table = newTab;
  20. return newTab;
  21. }

第二次及后续的resize执行流程

  1. final Node<K,V>[] resize() {
  2. Node<K,V>[] oldTab = table;
  3. // oldCap为当前table的长度, oldThr为上次的table临界值
  4. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  5. int oldThr = threshold;
  6. int newCap, newThr = 0;
  7. if (oldCap > 0) {
  8. // 这个if语句保证容量不超过hashmap的容量上限值。
  9. if (oldCap >= MAXIMUM_CAPACITY) {
  10. threshold = Integer.MAX_VALUE;
  11. return oldTab;
  12. }
  13. // 如果扩容之后,不超过容量上限,
  14. // 那么表的大小加倍。临界值加倍
  15. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  16. oldCap >= DEFAULT_INITIAL_CAPACITY)
  17. newThr = oldThr << 1; // double threshold
  18. }
  19. threshold = newThr;
  20. @SuppressWarnings({"rawtypes","unchecked"})
  21. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  22. table = newTab;
  23. //第二次扩容的时候,对上次的table如何处理。
  24. if (oldTab != null) {
  25. // 遍历之前的table,重新hash排序
  26. for (int j = 0; j < oldCap; ++j) {
  27. Node<K,V> e;
  28. //只对存在的索引操作
  29. if ((e = oldTab[j]) != null) {
  30. //销毁当前索引的内容
  31. oldTab[j] = null;
  32. //重新计算位置并赋值
  33. if (e.next == null)
  34. newTab[e.hash & (newCap - 1)] = e;
  35. //树的操作,下次再说
  36. else if (e instanceof TreeNode)
  37. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  38. //链表的操作
  39. else { // preserve order
  40. Node<K,V> loHead = null, loTail = null;
  41. Node<K,V> hiHead = null, hiTail = null;
  42. Node<K,V> next;
  43. do {
  44. next = e.next;
  45. // oldCap 为 16也就是 10000
  46. //oldCap为16的倍数,这里是hash值为低数字的时候
  47. if ((e.hash & oldCap) == 0) {
  48. //第一次
  49. if (loTail == null)
  50. loHead = e;
  51. //计算新的next
  52. else
  53. loTail.next = e;
  54. loTail = e;
  55. }
  56. //同理
  57. else {
  58. if (hiTail == null)
  59. hiHead = e;
  60. else
  61. hiTail.next = e;
  62. hiTail = e;
  63. }
  64. } while ((e = next) != null);
  65. if (loTail != null) {
  66. loTail.next = null;
  67. newTab[j] = loHead;
  68. }
  69. // 加上原本的偏移量
  70. if (hiTail != null) {
  71. hiTail.next = null;
  72. newTab[j + oldCap] = hiHead;
  73. }
  74. }
  75. }
  76. }
  77. }
  78. return newTab;
  79. }

resize中对有碰撞的链表的操作写的很有意思,再叙述一下。在重新分配索引的时候,有重新组建链表的操作。

举个比较夸张的例子,读者就明白了。

  • e.hash < 2,那么 e.hash&oldCap就等于0,索引为小于之前hash表大小以内的索引。也就是当初的索引不变。
  • e.hash > 2的时候,e.hash&old不等于0,那么它的索引就为当前表的索引再加上新扩容的大小。
img_6c8ebed5375d72c6f34b8e665fa3d6e1.png
案例图

这个图说的是,当hashmap的表大小为2扩充到4的时候,原本挂载在1位置的链表,重新分配之后的样子。

  1. Node<K,V> loHead = null, loTail = null;
  2. Node<K,V> hiHead = null, hiTail = null;
  3. Node<K,V> next;
  4. do {
  5. next = e.next;
  6. if ((e.hash & oldCap) == 0) {
  7. if (loTail == null)
  8. loHead = e;
  9. else
  10. loTail.next = e;
  11. loTail = e;
  12. }
  13. else {
  14. if (hiTail == null)
  15. hiHead = e;
  16. else
  17. hiTail.next = e;
  18. hiTail = e;
  19. }
  20. } while ((e = next) != null);
  21. if (loTail != null) {
  22. loTail.next = null;
  23. newTab[j] = loHead;
  24. }
  25. if (hiTail != null) {
  26. hiTail.next = null;
  27. newTab[j + oldCap] = hiHead;
  28. }

最后

篇幅有限,我这里仅仅介绍了get方法,put方法,resize方法的具体原理,文章就已经非常长了,不利于阅读。
下次再补充一下HashMap的hash方法原理,其余的相关注意事项。

参考

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

闽ICP备14008679号