HashMap的源码学习(jdk8)

╰半橙微兮° 2024-03-22 11:05 87阅读 0赞

hashmap的源码学习了,做个笔记

hashmap中的一些属性

  1. transient Node<K, V>[] table; // 数组,存放的为Node<K,V>
  2. int threshold; // 扩容的阈值,当前数组长度*扩容因子
  3. final float loadFactor; // 扩容因子
  4. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的大小 16
  5. static final int MAXIMUM_CAPACITY = 1 << 30; // 数组的最大空间
  6. /**
  7. * The load factor used when none specified in constructor.
  8. */
  9. static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认的负载因子大小
  10. static final int TREEIFY_THRESHOLD = 8; // 树化,链表需要达到阈值
  11. static final int UNTREEIFY_THRESHOLD = 6; // 退化为链表的阈值
  12. static final int MIN_TREEIFY_CAPACITY = 64; // 树化,数组需要达到的阈值

hashmap中的链表Node

  1. // 由代码可知此链表为单向的链表
  2. static class Node<K, V> implements Map.Entry<K, V> {
  3. final int hash; // 根据hash(key)计算出来的hash值,
  4. final K key;
  5. V value;
  6. Node<K, V> next; // 指向下一个节点
  7. Node(int hash, K key, V value, Node<K, V> next) {
  8. this.hash = hash;
  9. this.key = key;
  10. this.value = value;
  11. this.next = next;
  12. }
  13. public final K getKey() {
  14. return key;
  15. }
  16. public final V getValue() {
  17. return value;
  18. }
  19. public final String toString() {
  20. return key + "=" + value;
  21. }
  22. public final int hashCode() {
  23. return Objects.hashCode(key) ^ Objects.hashCode(value);
  24. }
  25. public final V setValue(V newValue) {
  26. V oldValue = value;
  27. value = newValue;
  28. return oldValue;
  29. }
  30. public final boolean equals(Object o) {
  31. if (o == this) {
  32. return true;
  33. }
  34. if (o instanceof Map.Entry) {
  35. Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
  36. if (Objects.equals(key, e.getKey()) &&
  37. Objects.equals(value, e.getValue())) {
  38. return true;
  39. }
  40. }
  41. return false;
  42. }
  43. }

put()方法

  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, boolean evict) {
  5. Node<K, V>[] tab;
  6. Node<K, V> p;
  7. int n, i;
  8. // eg1: table=null
  9. /** 如果是空的table,那么默认初始化一个长度为16的Node数组*/
  10. if ((tab = table) == null || (n = tab.length) == 0) {
  11. // eg1: resize返回(Node<K, V>[]) new Node[16],所以:tab=(Node<K, V>[]) new Node[16], n=16
  12. n = (tab = resize()).length;
  13. }
  14. /** 如果计算后的下标i,在tab数组中没有数据,那么则新增Node节点*/
  15. if ((p = tab[i = (n - 1) & hash]) == null) {
  16. // eg1: tab[0] = newNode(0, 0, "a0", null)
  17. // eg2: tab[1] = newNode(1, 1, "a1", null)
  18. tab[i] = newNode(hash, key, value, null);
  19. } else {
  20. /** 如果计算后的下标i,在tab数组中已存在数据,则执行以下逻辑 */
  21. Node<K, V> e;
  22. K k;
  23. // 判断key是否相等
  24. if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
  25. /** 如果与已存在的Node是相同的key值*/
  26. e = p;
  27. }
  28. // 判断是树还是链表
  29. else if (p instanceof TreeNode) {
  30. /** 如果与已存在的Node是相同的key值,并且是树节点*/
  31. e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
  32. } else {
  33. /** 如果与已存在的Node是相同的key值,并且是普通节点,则循环遍历链式Node,并对比hash和key,如果都不相同,则将新的Node拼装到链表的末尾。如果相同,则进行更新。*/
  34. for (int binCount = 0; ; ++binCount) {
  35. /** 获得p节点的后置节点,赋值给e。直到遍历到横向链表的最后一个节点,即:该节点的next后置指针为null */
  36. if ((e = p.next) == null) {
  37. // eg3: p.next = newNode(16, 16, "a16", null);
  38. // eg4-loop2: p.next == newNode(32, 32, "a32", null);
  39. // eg6: p.next == newNode(128, 128, "a128", null);
  40. p.next = newNode(hash, key, value, null);
  41. // eg3: binCount == 0
  42. // eg4-loop2: binCount == 1
  43. /** 是否要树化 ,binCount从0开始,横向链表中第2个node对应binCount=0,如果Node链表大于8个Node,那么试图变为红黑树 */
  44. if (binCount >= TREEIFY_THRESHOLD - 1) {
  45. // eg6: tab={newNode(0, 0, "a0", [指向后面1个链表中的7个node]), newNode(1, 1, "a1", null)}, hash=128
  46. treeifyBin(tab, hash); // 树化
  47. }
  48. break;
  49. }
  50. // eg4-loop1: e.hash==16 hash==32 所以返回false
  51. /** 更新操作,针对链表中的每个节点,都来判断一下,是否待插入的key与已存在的链表节点相同,如果相同,则跳出循环,并在后续的操作中,将该节点内容更新为最新的插入值 */
  52. if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
  53. break;
  54. }
  55. p = e;
  56. }
  57. }
  58. // 更新的操作
  59. /** 如果存在相同的key值*/
  60. if (e != null) {
  61. V oldValue = e.value;
  62. // onlyIfAbsent=false,HashMap初始化的参数,判断是否是不能修改,默认是false
  63. if (!onlyIfAbsent || oldValue == null) {
  64. /** 则将新的value值进行更新*/
  65. e.value = value;
  66. }
  67. afterNodeAccess(e); /** doing nothing */
  68. // 返回oldValue
  69. return oldValue;
  70. }
  71. }
  72. ++modCount; // 好像是并发用的
  73. // eg4: size=8, threshold=12
  74. if (++size > threshold) {
  75. resize();
  76. }
  77. afterNodeInsertion(evict); /** doing nothing */
  78. return null;
  79. }

get()方法

  1. public V get(Object key) {
  2. Node<K, V> e;
  3. // getNode(hash(key),key)得到对应的Node节点,在e.value获得值
  4. return (e = getNode(hash(key), key)) == null ? null : e.value;
  5. }
  6. // 这个操作我认为就是两点,一个是链表遍历查找,一个红黑树遍历查找
  7. final Node<K, V> getNode(int hash, Object key) {
  8. Node<K, V>[] tab;
  9. Node<K, V> first;
  10. Node<K, V> e;
  11. int n;
  12. K k;
  13. /**利用hash&(n-1)计算出数组下标 ,table存在元素 并且根据hash寻址后的下标位置也有元素存在 */
  14. if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
  15. /** hash值相同,并且key值也相同。真幸运,第一个元素就是待寻找的元素!!! */
  16. if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) {
  17. return first;
  18. }
  19. /** 向后寻找,开始遍历链表或者红黑树 */
  20. if ((e = first.next) != null) {
  21. // 方式一:在红黑树结构里寻找
  22. if (first instanceof TreeNode) {
  23. // 红黑树中查找
  24. return ((TreeNode<K, V>) first).getTreeNode(hash, key);
  25. }
  26. // 方式二:在链表结构里寻找
  27. do {
  28. if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
  29. return e;
  30. }
  31. } while ((e = e.next) != null);
  32. }
  33. }
  34. return null;
  35. }

总结一下,put()方法直接调用putVal(hash(key,key,value,false.true))方法,
hash(key)这个方法是计算一个hash值出来,主要的是它的函数扰动,也就是 (h = key.hashCode()) ^ (h >>> 16),用key的hashcode()和自己的高16位做异或运算,这样的好处是,使用这个值分布的更加分散,减少哈希冲突的出现。
在putVal方法中:
判断tab是不是null,长度是否为0,如果是那就扩容成初始大小16.
计算出这个key应该放入的数组下标,计算方法是:用hash(key)与数组大小减一做与运算,这样可以保证计算出来的值在数组大小之内(之所以不采用模运算,是因为‘与运算‘的效率是要高于模运算的)。
下标计算出来之后,判断下标处,是否已经有值,如果没有,便直接生成一个Node对象,放在这个位置。
这个位置有值,需要比较一下,key是否是相等的,如果是则做更新操作。
如果key的值不相等,那这可能就是那就要开始遍历,一种是遍历链表,一种是遍历红黑树。
遍历链表的话,就是比较key是否和链表上的key相等,如果没有相等的key,则尾插法,插入到链表中,如果有相等的key,将会和上面的走一样的更新操作。
遍历红黑树的话,就比较麻烦了,从根节点开始遍历,查找是否有和key相等的存在,存在便是更新操作,不存在则是将数据添加到红黑树的节点上,再做树的自旋操作,让树保持红黑树的特性。最好还要保证root节点放在数组中。
get()方法,主要是先通过调用hash()方法计算出hash值,确定key在数组中的下标,之后进行key的比较,如果下标位置的key相同,则直接返回这个Node节点,得到value值.
若是不相等,下一步则是遍历链表或者遍历红黑树(first instanceof TreeNode判断是红黑树还是链表),找到对应的Node节点,并返回。

本人对HashMap的源码理解有限,如有错误还望大家不吝赐教。

发表评论

表情:
评论列表 (有 0 条评论,87人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JDK1.8HashMap学习

    篇文章我们来学习一下HashMap的源码,这也是面试经常能问到的知识点,而之所以为什么面试会问到它,就是因为他的设计思想,是非常值得我们学习的,也是非常经典的。同时不同的...

    相关 JDK8HashMap

    > 进入这篇文章之前,我想清楚的说一说怎么去理解`HashMap`源码。它先是使用的`hash`算法,那么哈希算法需要注意的那就是怎么`hash`,怎么减少冲突,怎么避免冲突。

    相关 HashMap JDK8 分析

    概述 HashMap是基于哈希表(散列表),实现Map接口的双列集合,数据结构是“链表散列”,也就是数组+链表 ,key唯一的value可以重复,允许存储null 键nu

    相关 HashMap解析JDK1.8

     今天,打算写一篇HashMap的源码解析,主要是针对增删改查操作,废话不多说,直接开始。    先看看hashMap在jdk 1.8的结构,如下图,用的是数组+链表+红