码迷,mamicode.com
首页 > 编程语言 > 详细

Java HashMap 源码研究

时间:2021-01-14 11:29:05      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:for   cti   ade   mapping   abstract   转换   散列   ott   equals   

Jdk 8 的hashmap ,内部使用Node 表示数组成员,Node 实现了Map.Entry接口。

put() 过程:
 1 public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {
 2     // map 的元素就是保存在这个table数组变量之中,也就是“数组+链表” 之中的数组
 3     transient Node<K,V>[] table;
 4     
 5     public V put(K key, V value) {
 6         return putVal(hash(key), key, value, false, true); 
 7      }
 8     // hash 算法核心,减少碰撞
 9     static final int hash(Object key) {
10         int h;
11         /** 如果key是空,hash值返回0
12             否则,用key的hashcode() h变量 和 h无符号右移16位【右移一半】后的值【h是int类型32位,右移使hash值的高位变成低             位,举例1010 右移两位变成0010】作异或运算。这么做的目的是为了是哈希值更加散列【因为求key的下标是tab[i = (n -             1) & hash],用数组长度减一的值和哈希值做与运算,大多数情况下,哈希值参与运算仅为低位部分值,
13            举例:数组长度16,转换二进制:1111,哈希值为78897121,转换二进制:100101100111101111111100001与1111 运算如               下,结果是0000 0000 0000 0000 0000 0000 0000 0001,可见决定结果的是低4位,所以为了让高位也能参与到运算,减少            冲突。最后哈希值高位没变动,低位值被重新计算。】
14         */
15         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
16     }
17     // 散列码(哈希值)的获取,依赖本地方法,通常是将对象的内存地址转换为整数。
18     public native int hashCode();
19     // onlyIfAbsent:true 存在则不更新。evict:供LinkedHashMap 回调方法afterNodeInsertion()使用
20     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
21                    boolean evict) {
22         // tab 临时变量,p是桶位上的首个node,n添加元素后的数组长度,i是目标位置下标
23         Node<K,V>[] tab; Node<K,V> p; int n, i;
24         if ((tab = table) == null || (n = tab.length) == 0)
25             n = (tab = resize()).length;
26         // 如果之前桶位上没有值,那么构造一个node 放上去
27         if ((p = tab[i = (n - 1) & hash]) == null)
28             tab[i] = newNode(hash, key, value, null);
29         else {
30             // 到这里说明桶位上有值,要么添加自己到链表/红黑树末尾,要么找到和自己key一致的node,替换node的值
31             // e 是存在的那个节点,k 是存在的节点的key
32             Node<K,V> e; K k;
33             // 如果p-原来的node的哈希值和新值的hash值一致,且内容也一样。则p就是新值要替代对象,赋给e
34             if (p.hash == hash &&
35                 ((k = p.key) == key || (key != null && key.equals(k))))
36                 e = p;
37             // 如果p已经不是node结构【链表结构】,而是红黑树
38             else if (p instanceof TreeNode) 
39                 // 调用红黑树的putTreeVal(),返回要替代的目标节点
40                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
41             else {
42                 // 走到这里,说明目前还是数组+链表结构,且桶位上p并不是我们要替代的目标,所以开始遍历链表
43                 for (int binCount = 0; ; ++binCount) {
44                     // 如果p的下一个结点不存在,说明遍历链表完毕,那好,就构造一个新节点插入到链尾
45                     if ((e = p.next) == null) {
46                         p.next = newNode(hash, key, value, null);
47                         // 如果目前桶位上链表数超过 树化阈值,那么就转为红黑树
48                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
49                             treeifyBin(tab, hash);
50                         break;
51                     }
52                     // 到这里说明e不为空,判断当前结点e和新值的哈希值和key内容是否相同
53                     // 如果成立,好这就是要更新替代的目标节点,跳出循环,不再遍历后续链表
54                     if (e.hash == hash &&
55                         ((k = e.key) == key || (key != null && key.equals(k))))
56                         break;
57                     p = e;
58                 }
59             }
60             // 如果e不为空,就返回老值,保存新值。
61             // 如果e为空,说明之前已经构造新值并插入链表末尾
62             if (e != null) { // existing mapping for key
63                 V oldValue = e.value;
64                 // onlyIfAbsent 方法形参,true:不能修改存在的值。控制是否可以修改值的一个开关
65                 // 如果允许修改或者老值为空,则赋新值
66                 if (!onlyIfAbsent || oldValue == null)
67                     e.value = value;
68                 afterNodeAccess(e);// 回调方法,空实现,模板模式
69                 return oldValue;
70             }
71         }
72         // 修改计数器加一
73         ++modCount;
74         // 走到这一步,说明新元素已经被构造成一个Node,添加到了链尾,最后返回值为空,因为新元素是新加入,没有替代谁【本方法说的替代准确说是替代老节点的value的值,而不是老节点】。
75         // 如果hashmap添加新元素后,实际使用长度超过扩容阈值,完蛋,扩容吧。
76         if (++size > threshold)
77             resize();
78         afterNodeInsertion(evict);// 回调方法,空实现,模板模式
79         return null;
80     }
81     // Callbacks to allow LinkedHashMap post-actions
82     void afterNodeAccess(Node<K,V> p) { }
83     void afterNodeInsertion(boolean evict) { }
84     
85     // 树化阈值,如果链表结点数大于等于8,就会转为红黑树
86     static final int TREEIFY_THRESHOLD = 8;
87     // 链化阈值,如果红黑树结点数小于等于6,就会转为链表
88     static final int UNTREEIFY_THRESHOLD = 6;
89 
90 
91 }
扩容过程 resize(),是Java 8 HashMap更改最大的部分,Jdk7 是使用头插法,多线程情况下会导致循环引用
  1 // 扩容方法,面试重点
  2 final Node<K,V>[] resize() {
  3     Node<K,V>[] oldTab = table;
  4     int oldCap = (oldTab == null) ? 0 : oldTab.length;
  5     int oldThr = threshold;
  6     int newCap, newThr = 0;
  7     if (oldCap > 0) {
  8         // 如果之前的容量大于等于1<<30 即1073741824;那么新扩容阈值变为2147483647,这个值要比容量大,
  9         if (oldCap >= MAXIMUM_CAPACITY) {
 10             threshold = Integer.MAX_VALUE;
 11             return oldTab;
 12         }
 13         // 如果之前容量扩大一倍后赋给新容量 小于最大容量,且老容量大于等于默认初始化容量16,新扩容阈值也变大
 14         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
 15                  oldCap >= DEFAULT_INITIAL_CAPACITY)
 16             newThr = oldThr << 1; // double threshold
 17     }
 18     // 如果oldCap <= 0,且oldThr > 0,那么初始容量设置为阈值
 19     else if (oldThr > 0) // initial capacity was placed in threshold
 20         newCap = oldThr;
 21     else {               // zero initial threshold signifies using defaults
 22         newCap = DEFAULT_INITIAL_CAPACITY;
 23         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
 24     }
 25     // 如果新的扩容阈值等于0,重新计算扩容阅知
 26     if (newThr == 0) {
 27         float ft = (float)newCap * loadFactor;
 28         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
 29                   (int)ft : Integer.MAX_VALUE);
 30     }
 31     threshold = newThr;
 32     // 以新的容量构造一个数组
 33     @SuppressWarnings({"rawtypes","unchecked"})
 34     Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
 35     // map的数组引用指向新数组【扩容后数组】
 36     table = newTab;
 37     if (oldTab != null) {
 38         // 遍历原数组
 39         for (int j = 0; j < oldCap; ++j) {
 40             Node<K,V> e;
 41             // 如果原数组桶位上 存在节点node,不为空
 42             if ((e = oldTab[j]) != null) {
 43                 // 桶位上的node已经赋值到e,清空桶位
 44                 oldTab[j] = null;
 45                 /** 如果e的下一个节点不存在,也就是说链表只有一个节点,直接将e这个放到新数组的桶位上,
 46                 桶位的下标计算公式不变,唯一区别是使用新数组的容量-1来与运算。newCap是oldCap 的2倍,
 47                 比如 oldCap为16,oldCap-1→1111,newCap为32,newCap-1→11111。此时e.hash值的第三位决定了,新桶                        位的下标,如果e.hash的第三位是0,则计算出的下标和之前一样,可以说位置不变。如果是1,新下标就会变                            成11111,可分解为 原下标1111【15】+10000【16】,原下标15,新下标31。总结就是下标要么在原址,
 48                 要么跑到原址+oldCap【原来容量】的地址
 49                 */
 50                 if (e.next == null)
 51                     newTab[e.hash & (newCap - 1)] = e;
 52                 else if (e instanceof TreeNode)
 53                     // 如果e 是红黑树节点,调用split()方法来处理
 54                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
 55                 else { // preserve order
 56                     // 到这里说明,桶位上是链表,且链表结点不为1.
 57                     // loHead、loTail 是扩容后在落在原址的结点组成的链表的链表头尾,称之为低位链
 58                     // hiHead、hiTail 是扩容后到落在原址+oldCap的地址组成的链表的链表头尾,称之为高位链。
 59                     Node<K,V> loHead = null, loTail = null;
 60                     Node<K,V> hiHead = null, hiTail = null;
 61                     // next 临时变量,代表下一个被处理的节点
 62                     Node<K,V> next;
 63                     // 开始遍历待分配位置链表
 64                     do {
 65                         // 当前结点e的下一个结点地址赋给next
 66                         // ? next变量 能否省略。我的感觉是可以,退出条件改为while((e=e.next) !=null)
 67                         next = e.next;
 68                         /* 判断当前结点的哈希值和扩容前容量做与运算是否等于0,目的是为了将链表上的节点均匀的分配到低位链                                高位链。它是如何做到的呢?oldCap二进制一定是100~这种形式,做与运算只需判断e.hash二进制值和                               前者1所对应的位置上的数是0还是1,是0当前结点e加入低位链。是1当前节点e加入高位链。
 69                          注意:一个链表上的不同结点的散列码是不同的,来自不同的内存地址。
 70                         */
 71                         if ((e.hash & oldCap) == 0) {
 72                             // 如果是首次向低位链添加,就把e赋值给loHead链表头
 73                             if (loTail == null)
 74                                 loHead = e;
 75                             else
 76                                 // 不是第一次向低位链添加结点,那么就赋值给loTail.next,链接到链表尾部【尾插法】
 77                                 loTail.next = e;
 78                             // 更新低位链后,loTail重新指向链尾。【就像一个箭头下移了一位】
 79                             loTail = e;
 80                         }
 81                         // 执行else,说明结点e应该被添加到高位链上。
 82                         else {
 83                             // 如果是首次向高位链添加,就把e赋值给hiHead链表头
 84                             if (hiTail == null)
 85                                 hiHead = e;
 86                             else
 87                                 // 不是第一次向高位链添加结点,那么就赋值给hiTail.next,链接到链表尾部【尾插法】
 88                                 hiTail.next = e;
 89                             // 更新高位链后,hiTail重新指向链尾。【就像一个箭头下移了一位】
 90                             hiTail = e;
 91                         }
 92                     // 遍历老链表,当next 为null时,退出循环    
 93                     } while ((e = next) != null);
 94                     // 遍历完成后,待分配节点已经各自组成了高位链、低位链
 95                     if (loTail != null) {
 96                         // 低位链链尾指向null,把链表放置到新数组老位置
 97                         loTail.next = null;
 98                         newTab[j] = loHead;
 99                     }
100                     if (hiTail != null) {
101                         // 高位链链尾指向null,把链表放置到新数组新位置,相对老位置偏移oldCap位。
102                         hiTail.next = null;
103                         newTab[j + oldCap] = hiHead;
104                     }
105                 }
106             }
107             // 每次循环搞定一个桶位
108         }
109     }
110     // 最终返回扩容后数组
111     return newTab;
112 }

get()方法下回补充。。

 

Java HashMap 源码研究

标签:for   cti   ade   mapping   abstract   转换   散列   ott   equals   

原文地址:https://www.cnblogs.com/houj/p/14275177.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有
迷上了代码!