HashMap源码记录1

前言:由于笔者所在的公司用的是jdk1.8,故该源码是针对1.8分析的。

首先:我们看一张长的很丑的HashMap的结构图:

 再看看几个核心的常量:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   // 默认初始化容器大小,当自己指定容器大小时,必须为2的幂次方

static final int MAXIMUM_CAPACITY = 1 << 30;          //容器的最大容量值

static final float DEFAULT_LOAD_FACTOR = 0.75f;       //加载因子

static final int TREEIFY_THRESHOLD = 8;               //把桶中链表转换为红黑树的阈值

static final int UNTREEIFY_THRESHOLD = 6;             //把红黑树转换成链表的阈值

static final int MIN_TREEIFY_CAPACITY = 64;           //最小的表容量使桶被树化,当表的容量小于该值时,先扩容解决hash冲突而不是树化

 putVal()方法:

 1 /**
 2      * 实现Map.put和相关方法
 3      *
 4      * @param hash 根据key计算出来的hash值
 5      * @param key 键
 6      * @param value 值
 7      * @param onlyIfAbsent 如果为true则不改变value值,即如果不存在则添加。
 8      * @param evict 如果false,表处于创建模式,put方法给的是true
 9      * @return 返回旧值,如果不存在则返回null
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) == 0)             //如果表为空,调用resize()初始化,该方法的分析在下面,建议先把putValue方法看完
15             n = (tab = resize()).length;
16         if ((p = tab[i = (n - 1) & hash]) == null)                      //根据当前key的hashcode定位到具体的桶中并判断是否为空,如果为空,创建一个节点并复制给该桶
17             tab[i] = newNode(hash, key, value, null);
18         else {
19             Node<K,V> e; K k;
20             if (p.hash == hash &&
21                 ((k = p.key) == key || (key != null && key.equals(k)))) //如果当前桶的key的hashcode与要设置的key的相同,且键值相等,用要插入的key对应的value覆盖原来key的value
22                 e = p;
23             else if (p instanceof TreeNode)                      //如果当前桶为红黑树,则按照红黑树的方式写入数据,分析在下
24                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
25             else {                                                    //表示当前桶为链表,不是红黑树
26                 for (int binCount = 0; ; ++binCount) {                //遍历链表
27                     if ((e = p.next) == null) {                       //当遍历到链表中的最后一个元素
28                         p.next = newNode(hash, key, value, null);      //把要插入的key、value封装成一个新的节点写入当前桶的后面
29                         if (binCount >= TREEIFY_THRESHOLD - 1)         // 当该链表的值大于树形化的预设阈值时,需要将该链表转换为红黑树
30                             treeifyBin(tab, hash);                  //树形化方法
31                         break;
32                     }
33                     if (e.hash == hash &&                           //在遍历链表的过程中发现有与要设置的key相同hashcode和键值的key,直接覆盖原来的key中的value,即更新value
34                         ((k = e.key) == key || (key != null && key.equals(k))))
35                         break;
36                     p = e;
37                 }
38             }
39             if (e != null) {                                           // 用要插入的key称为key1的value值覆盖原来桶中与key1的hashcode和键值相等的key的value
40                 V oldValue = e.value;
41                 if (!onlyIfAbsent || oldValue == null)
42                     e.value = value;
43                 afterNodeAccess(e);
44                 return oldValue;
45             }
46         }
47         ++modCount;
48         if (++size > threshold)                                      //当前表中的桶的个数大于阈值时,扩容当前表
49             resize();                                            //扩容(resize方法中包含了初始化和扩容)
50         afterNodeInsertion(evict);
51         return null;
52     }  

resize()方法:

 1 /**
 2      *初始化或增加表大小。 如果为空,则根据字段阈值中保持的初始容量目标进行分配。 
 3      *即初始化 。扩容时,因为表的容量使用的是2的幂,所以每个桶中的元素必须保持相 
 4      *同的索引,或者在新表中以2的幂偏移,即扩容的操作。
 5      * @return the table
 6      */
 7     final Node<K,V>[] resize() {
 8         Node<K,V>[] oldTab = table;
 9         int oldCap = (oldTab == null) ? 0 : oldTab.length;
10         int oldThr = threshold;                                // threshold表示阈值,当new HashMap时未指定容量大小时,该值为DEFAULT_INITIAL_CAPACITY
11         int newCap, newThr = 0;
12         if (oldCap > 0) {                                                     // 原始hash表的容量不为零,即进行扩容操作
13             if (oldCap >= MAXIMUM_CAPACITY) {                                 // 原始hash表的容量已经达到最大容量
14                 threshold = Integer.MAX_VALUE;
15                 return oldTab;
16             }
17             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&          // 把原始hash表的容量扩大一倍得到新的容量
18                      oldCap >= DEFAULT_INITIAL_CAPACITY)
19                 newThr = oldThr << 1;                                         // 把旧的阈值扩大一倍赋值给新的阈值  
20         }
21         else if (oldThr > 0)                                                  // 原始hash表的容量为0且指定了阈值时,初始化时把hash表的容量设置为初始阈值
22             newCap = oldThr;
23         else {                                                                // 未指定初始化阈值时,使用默认的值
24             newCap = DEFAULT_INITIAL_CAPACITY;
25             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
26         }
27         if (newThr == 0) {                     
28             float ft = (float)newCap * loadFactor;
29             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
30                       (int)ft : Integer.MAX_VALUE);
31         }
32         threshold = newThr;
33         @SuppressWarnings({"rawtypes","unchecked"}) 
34             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
35         table = newTab;
36         if (oldTab != null) {                                               // 扩容操作
37             for (int j = 0; j < oldCap; ++j) {                  // 遍历原始hash表的每一个桶
38                 Node<K,V> e;
39                 if ((e = oldTab[j]) != null) {
40                     oldTab[j] = null;                        // 把原始hash表的桶置为空
41                     if (e.next == null)
42                         newTab[e.hash & (newCap - 1)] = e;                  // 如果hash表中的第j个桶为空,通过hash算法计算出e在新的hash表中的位置,并把e放入该桶中
43                     else if (e instanceof TreeNode)                         // 如果hash表中的第j个桶是红黑树,则以红黑树的规则添加元素
44                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
45                     else {                                                  // 保留顺序
46                         Node<K,V> loHead = null, loTail = null;             // loHead:用于指向原
47                         Node<K,V> hiHead = null, hiTail = null;
48                         Node<K,V> next;
49                         do {
50                             next = e.next;
51                             if ((e.hash & oldCap) == 0) {                   // (e.hash & oldCap) == 0表示元素位置在扩容后不变
52                                 if (loTail == null)                         //该段逻辑的主要作用是让loHead指向不需移动位置的元素组成的链表的头,loTail指向尾
53                                     loHead = e;
54                                 else
55                                     loTail.next = e;
56                                 loTail = e;
57                             }
58                             else {                                          // 元素位置在扩容后位置发生变化,新的下标位置为原下标位置+原数组长度
59                                 if (hiTail == null)                // 该段逻辑主要作用是让hiHead指向需要移动位置的元素组成的链表的头,hiTail指向尾
60                                     hiHead = e;                  
61                                 else
62                                     hiTail.next = e;
63                                 hiTail = e;
64                             }
65                         } while ((e = next) != null);
66                         if (loTail != null) {                  
67                             loTail.next = null;                  // 链表的最后一个元素指向null
68                             newTab[j] = loHead;                  // 把不需要改变位置的链表赋值到新的hash表中,位置为原数组下标
69                         }
70                         if (hiTail != null) {    
71                             hiTail.next = null;                  // 链表的最后一个元素指向null
72                             newTab[j + oldCap] = hiHead;            // 把需要改变位置的链表赋值到新的hash表中,位置为原下标位置+原数组长度
73                         }
74                     }
75                 }
76             }
77         }
78         return newTab;
79     } 

 *注:以上代码中loHead用来指向hash表扩容后元素位置不变的链表头,loTail指向链表尾;hiHead用来指向hash表扩容后元素位置变化的链表头,hiTail指向链表尾。

*例子:假设有一个hash表h,长度为8(oldcap),现有hashcode分别为3,11 , 19 的三个元素要插入hash表中,通过hash算法 (e.hashcode & (oldCap - 1))得到元素下标位置都为3。扩容后通过

(e.hashcode & oldcap)得到的是元素是否需要移动(0表示需要,1表示不需要),以上三个元素经过计算得到的值分别为0,1,0,此时3和19组成新的链表,而loHead指向该链表的头,liTail指向该

链表的尾;11单独成一个新的链表,hiHead指向该链表的头,hiTail指向该链表的尾部。

putTreeVal()方法:

 1 /**
 2  * 当存在hash碰撞的时候,且元素数量大于8个(hash表的容量也需要大于64,否则会做扩容操作)时候,就会以红黑树的方式将这些元素组织起来
 3  * map 当前节点所在的HashMap对象
 4  * tab 当前HashMap对象的元素数组
 5  * h   指定key的hash值
 6  * k   指定key
 7  * v   指定key上要写入的值
 8  * 返回:指定key所匹配到的节点对象,针对这个对象去修改V(返回空说明创建了一个新节点)
 9  */
10 final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
11                                 int h, K k, V v) {
12     Class<?> kc = null;                                    // 定义k的Class对象
13     boolean searched = false;                              // 标识是否已经遍历过一次树,未必是从根节点遍历的,但是遍历路径上一定已经包含了后续需要比对的所有节点。
14     TreeNode<K,V> root = (parent != null) ? root() : this; // 父节点不为空那么查找根节点,为空那么自身就是根节点
15     for (TreeNode<K,V> p = root;;) {                       // 从根节点开始遍历,没有终止条件,只能从内部退出
16         int dir, ph; K pk;                                 // 声明方向、当前节点hash值、当前节点的键对象
17         if ((ph = p.hash) > h)                             // 如果当前节点hash 大于 指定key的hash值
18             dir = -1;                                      // 要添加的元素应该放置在当前节点的左侧
19         else if (ph < h)                                   // 如果当前节点hash 小于 指定key的hash值
20             dir = 1;                                       // 要添加的元素应该放置在当前节点的右侧
21         else if ((pk = p.key) == k || (k != null && k.equals(pk))) // 如果当前节点的键对象 和 指定key对象相同
22             return p;                                      // 那么就返回当前节点对象,在外层方法会对v进行写入
23  
24         // 走到这一步说明 当前节点的hash值  和 指定key的hash值  是相等的,但是equals不等
25         else if ((kc == null &&
26                     (kc = comparableClassFor(k)) == null) ||
27                     (dir = compareComparables(kc, k, pk)) == 0) {
28  
29             // 走到这里说明:指定key没有实现comparable接口   或者   实现了comparable接口并且和当前节点的键对象比较之后相等(仅限第一次循环)
30         
31  
32             /*
33              * searched 标识是否已经对比过当前节点的左右子节点了
34              * 如果还没有遍历过,那么就递归遍历对比,看是否能够得到那个键对象equals相等的的节点
35              * 如果得到了键的equals相等的的节点就返回
36              * 如果还是没有键的equals相等的节点,那说明应该创建一个新节点了
37              */
38             if (!searched) { // 如果还没有比对过当前节点的所有子节点
39                 TreeNode<K,V> q, ch; // 定义要返回的节点、和子节点
40                 searched = true; // 标识已经遍历过一次了
41                 /*
42                  * 红黑树也是二叉树,所以只要沿着左右两侧遍历寻找就可以了
43                  * 这是个短路运算,如果先从左侧就已经找到了,右侧就不需要遍历了
44                  * find 方法内部还会有递归调用。参见:find方法解析
45                  */
46                 if (((ch = p.left) != null &&
47                         (q = ch.find(h, k, kc)) != null) ||
48                     ((ch = p.right) != null &&
49                         (q = ch.find(h, k, kc)) != null))
50                     return q; // 找到了指定key键对应的
51             }
52  
53             // 走到这里就说明,遍历了所有子节点也没有找到和当前键equals相等的节点
54             dir = tieBreakOrder(k, pk); // 再比较一下当前节点键和指定key键的大小
55         }
56  
57         TreeNode<K,V> xp = p; // 定义xp指向当前节点
58         /*
59         * 如果dir小于等于0,那么看当前节点的左节点是否为空,如果为空,就可以把要添加的元素作为当前节点的左节点,如果不为空,还需要下一轮继续比较
60         * 如果dir大于等于0,那么看当前节点的右节点是否为空,如果为空,就可以把要添加的元素作为当前节点的右节点,如果不为空,还需要下一轮继续比较
61         * 如果以上两条当中有一个子节点不为空,这个if中还做了一件事,那就是把p已经指向了对应的不为空的子节点,开始下一轮的比较
62         */
63         if ((p = (dir <= 0) ? p.left : p.right) == null) {  
64             // 如果恰好要添加的方向上的子节点为空,此时节点p已经指向了这个空的子节点
65             Node<K,V> xpn = xp.next; // 获取当前节点的next节点
66             TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); // 创建一个新的树节点
67             if (dir <= 0)
68                 xp.left = x;  // 左孩子指向到这个新的树节点
69             else
70                 xp.right = x; // 右孩子指向到这个新的树节点
71             xp.next = x;      // 链表中的next节点指向到这个新的树节点
72             x.parent = x.prev = xp; // 这个新的树节点的父节点、前节点均设置为 当前的树节点
73             if (xpn != null)  // 如果原来的next节点不为空
74                 ((TreeNode<K,V>)xpn).prev = x; // 那么原来的next节点的前节点指向到新的树节点
75             moveRootToFront(tab, balanceInsertion(root, x));// 重新平衡,以及新的根节点置顶
76             return null;      // 返回空,意味着产生了一个新节点
77         }
78     }
79 }

以上是对HashMap中putValue的分析,其它的方法需要有空的时候再写了。

猜你喜欢

转载自www.cnblogs.com/noah-sheng/p/12074511.html