HashMap源码分析——基于jdk1.8

前言:上篇文章,笔者分析了jdk1.7中HashMap的源码,这里将对jdk1.8的HashMap的源码进行分析。

注:本文jdk源码版本为jdk1.8.0_172


1.再看put操作

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

jdk1.8中的hash算法:

1  static final int hash(Object key) {
2         int h;
3         // 这里的hash算法和jdk1.7中很不一样,直接高16位与低16位做异或,这样的话高低位都进行了运算,增加hash值的随机性
4         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
5     }

再看put操作的核心函数:

 1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                    boolean evict) {
 3         // jdk1.8中HashMap底层数据结构使用的是Node
 4         Node<K,V>[] tab; Node<K,V> p; int n, i;
 5         // 如果table还未初始化,则初始化table,注意这里初始化使用的是resize函数[扩容函数]
 6         if ((tab = table) == null || (n = tab.length) == 0)
 7             n = (tab = resize()).length;
 8         /**
 9          * 这里表示如果tab[i]位置上为null,则直接插入数据
10          * i=(n-1)&hash与jdk1.7中找出元素在tab上的index是一样的操作
11          * 注意这里在多线程环境下会造成线程不安全问题
12          */
13         if ((p = tab[i = (n - 1) & hash]) == null)
14             tab[i] = newNode(hash, key, value, null);
15         else {// 如果i位置上有元素,则进行链式存储
16             Node<K,V> e; K k;
17             // 如果tab[i]上的元素与插入元素的key完全一样,则进行覆盖操作
18             if (p.hash == hash &&
19                     ((k = p.key) == key || (key != null && key.equals(k))))
20                 e = p;
21             // 判断当前元素是否是红黑树结构
22             else if (p instanceof TreeNode)
23                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
24             else {
25                 for (int binCount = 0; ; ++binCount) {
26                     // 如果p节点的next为空,则将待插入的元素,直接添加在链表尾
27                     if ((e = p.next) == null) {
28                         // 从这里可知道jdk1.8在,如果存在链表,插入数据是直接放在链表尾的
29                         p.next = newNode(hash, key, value, null);
30                         // 当同一节点链表中元素个数>=8时,底层数据结构转向红黑树,
31                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
32                             treeifyBin(tab, hash);  // 将底层数据结构转向红黑树
33                         break;
34                     }
35                     // 判断next元素是否和插入元素相同,如果相同,则不做操作,跳出循环
36                     if (e.hash == hash &&
37                             ((k = e.key) == key || (key != null && key.equals(k))))
38                         break;
39                     p = e; // 将next赋值给p,继续循环
40                 }
41             }
42             // 覆盖操作
43             if (e != null) { // existing mapping for key
44                 V oldValue = e.value;
45                 // onlyIfAbsent表示是否要改变原来的值,true-不改变,false-改变
46                 if (!onlyIfAbsent || oldValue == null)
47                     e.value = value;
48                 afterNodeAccess(e);
49                 return oldValue;
50             }
51         }
52         // 修改次数加1,fail-fast机制
53         ++modCount;
54         // 判断是否需要扩容
55         if (++size > threshold)
56             resize();
57         afterNodeInsertion(evict);
58         return null;
59     }

重点:

jdk1.8中HashMap在进行put操作时:

#1.如果同一hash值的节点数小于8时,底层数据结构仍然是链表;当节点数大于等于8时,会转向红黑树。

#2.如果节点的数据结构是链表时,插入数据是直接放在链表尾的,从而避免插入元素时,形成环形链,造成死循环。

put操作的核心代码中,还涉及一个比较重要的函数,这里进行详细分析。

#resize()

  1 final Node<K,V>[] resize() {
  2         Node<K,V>[] oldTab = table;
  3         int oldCap = (oldTab == null) ? 0 : oldTab.length;
  4         int oldThr = threshold;
  5         int newCap, newThr = 0;
  6         // 如果table中有元素
  7         if (oldCap > 0) {
  8             // 容量是否已达限制
  9             if (oldCap >= MAXIMUM_CAPACITY) {
 10                 threshold = Integer.MAX_VALUE;
 11                 return oldTab;
 12             }
 13             // 扩容,并更新扩容阈值
 14             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
 15                     oldCap >= DEFAULT_INITIAL_CAPACITY)
 16                 newThr = oldThr << 1; // double threshold
 17         }
 18         // 如果table中没有元素,但是已初始化扩容阈值,这里将table的新容量赋值为扩容阈值
 19         else if (oldThr > 0) // initial capacity was placed in threshold
 20             newCap = oldThr;
 21             // 如果以上条件都不满足,则利用默认值进行初始化
 22         else {               // zero initial threshold signifies using defaults
 23             newCap = DEFAULT_INITIAL_CAPACITY;
 24             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
 25         }
 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         // 如果原来table有值,则循环将原值转移到newTab中
 37         if (oldTab != null) {
 38             for (int j = 0; j < oldCap; ++j) {
 39                 Node<K,V> e;
 40                 // 找到有值的节点
 41                 if ((e = oldTab[j]) != null) {
 42                     oldTab[j] = null; //将原来table中当前位置置null
 43                     if (e.next == null) // 如果当前节点next为null,将其放置在newTab中的新位置
 44                         newTab[e.hash & (newCap - 1)] = e;
 45                         // 如果是红黑树则进行红黑树操作,关于红黑树后面会进行分析
 46                     else if (e instanceof TreeNode)
 47                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
 48                     else { // preserve order  // 当走到这里,说明节点上为链表形式存储数据,需进行循环操作
 49                         // 存储位置在newTable和oldTable位置不变元素
 50                         Node<K,V> loHead = null, loTail = null;
 51                         // 存储oldTable中位置发生了变化的元素,当然这里是和oldTable相比较
 52                         // 参看下面的注释,应该可以很好理解
 53                         Node<K,V> hiHead = null, hiTail = null;
 54                         Node<K,V> next;
 55                         do {
 56                             // 由于是链表循环,因此需存储next节点的值,这种形式在jdk1.7中出现过多次
 57                             next = e.next;
 58                             /**
 59                              *这里需要注意一下,这里是用元素的hash值,与原来table长度做&操作
 60                              * 如果为0,则表示e.hash&(newCap-1)和e.hash&(oldCap-1)是一样的
 61                              * 也就说元素的位置在newTable中是不变的,因为newTable的大小为oldTable大小的2倍
 62                              * 相当于其二进制向左移动了1位,其newCap-1的二进制全都为1,且比原来oldCap-1的二进制多了一个1
 63                              * eg:oldCap=16,newCap=32,注意求key的位置是用e.hash&(table.length-1)
 64                              * e.hash&0x1111=原来key的位置
 65                              * e.hash&0x10000=0,表明e.hash在二进制的第5位上一定为0,所以:
 66                              * e.hash&0x11111=也一定是原来key的位置
 67                              * 如果:
 68                              * e.hash&0x10000=1,表明e.hash在二进制的第5位上一定为1,所以:
 69                              * e.hash&0x11111=原来key的位置加上oldCap的长度即可(0x10000)
 70                              * 这样根据一个二进制位就将原来的一条链表分成两条链表进行存储,这里非常的关键,不是很好理解
 71                              * 仔细理解上面的解释,相信你会发现这是非常神奇的一个技巧
 72                              */
 73                             // 有了上面的原理,再来看这就非常明确了
 74                             // 元素在newTable中位置不改变
 75                             if ((e.hash & oldCap) == 0) {
 76                                 // 初始时,将e放在loHead头上,然后尾又是e,后续循环的时候,只操作tail就行了,形成链表
 77                                 if (loTail == null)
 78                                     loHead = e;
 79                                 else
 80                                     loTail.next = e;
 81                                 // 尾部存储为e,形成链表,注意理解就好
 82                                 loTail = e;
 83                             }
 84                             // 元素在newTable中位置发生了变化[相对oldTable]
 85                             // 这里就相当于两条链表了,位置不变的一条,位置变了的又是一条
 86                             else {
 87                                 if (hiTail == null)
 88                                     hiHead = e;
 89                                 else
 90                                     hiTail.next = e;
 91                                 hiTail = e;
 92                             }
 93                         } while ((e = next) != null);
 94                         // 如果位置不变链表不为null
 95                         if (loTail != null) {
 96                             loTail.next = null;
 97                             // 从这里也可看出这里存储的是元素在newTable中位置不改变[相对oldTable]
 98                             // 只需要存储head值即可,因为已形成链表
 99                             newTab[j] = loHead;
100                         }
101                         if (hiTail != null) {
102                             hiTail.next = null;
103                             // 位置变化的元素,位置只需要加上oldCap的值就可以了,上面已进行分析
104                             newTab[j + oldCap] = hiHead;
105                         }
106                     }
107                 }
108             }
109         }
110         return newTab;
111     }

重点:

resize函数中在对节点元素存在链表时的处理有点小技巧,虽然是再次hash,但它是根据key在oldTable中位置与newTable的位置来进行区分的,变成两条链表存储,具体分析过程在函数中已注释写得非常详细。

jdk1.8中HashMap主要在底层增加了红黑树的数据结构,关于红黑树这种数据结构笔者还是有点懵懂,后面会专门深入这方面的知识点。

2.再看get操作

1 public V get(Object key) {
2         // 这里与jdk1.7的get方法有很大的不一样
3         Node<K,V> e;
4         // 通过getNode方法,如果e不为null,则返回e的value,否则返回null
5         return (e = getNode(hash(key), key)) == null ? null : e.value;
6     }

get关键函数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         // 如果HashMap中有值,其当前元素在table中有值,则进行寻找
 4         if ((tab = table) != null && (n = tab.length) > 0 &&
 5             (first = tab[(n - 1) & hash]) != null) {
 6             // 注释已经说的非常清楚了,检查第一个节点的值是否与要查找的相同,快速return
 7             if (first.hash == hash && // always check first node
 8                 ((k = first.key) == key || (key != null && key.equals(k))))
 9                 return first;
10             // 如果当前节点下存在红黑树或链表结构,则进行循环操作    
11             if ((e = first.next) != null) {
12                 // 如果next节点为红黑树则在红黑树中查找元素
13                 if (first instanceof TreeNode)
14                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);
15                 // 走到该分支,则表明当前节点下为链表结构,下面的循环就比较简单了,通过循环不断的查找相同的元素,有则返回,无则返回null
16                 do {
17                     if (e.hash == hash &&
18                         ((k = e.key) == key || (key != null && key.equals(k))))
19                         return e;
20                 } while ((e = e.next) != null);
21             }
22         }
23         // 否则直接返回null
24         return null;
25     }

分析:

#1.get函数的核心代码逻辑还是非常简单的,注意:总是首先校验头结点,快速return

#2.其次是红黑树中查找元素,由于红黑树的数据结构相对较复杂,后续在进行相应分析。

3.其他重要函数

#removeNode该函数为remove函数的核心函数:

 1 /**
 2      * HashMap删除元素
 3      * @param hash key的hash值
 4      * @param key  传入的删除的key值
 5      * @param value 值,传入为nul
 6      * @param matchValue if true only remove if value is equal 如果为true,则只移除value相等的元素,所以这里传入false
 7      * @param movable if false do not move other nodes while removing 该值主要用于红黑树移除节点后,再重塑红黑树,所以传入true
 8      * @return
 9      */
10     final Node<K,V> removeNode(int hash, Object key, Object value,
11                                boolean matchValue, boolean movable) {
12         Node<K,V>[] tab; Node<K,V> p; int n, index;
13         // 同样先判断是否存在该元素,并记录头结点元素p
14         if ((tab = table) != null && (n = tab.length) > 0 &&
15                 (p = tab[index = (n - 1) & hash]) != null) {
16             Node<K,V> node = null, e; K k; V v;
17             // 如果头结点直接命中,则用node存储下来
18             if (p.hash == hash &&
19                     ((k = p.key) == key || (key != null && key.equals(k))))
20                 node = p;
21             // 该分支表示头结点未命中,判断p结点的next是否不为null,因为要循环,所以需要将next元素记录下来,这种方式在HashMap的循环中很常用
22             else if ((e = p.next) != null) {
23                 // 如果p为红黑树结构,则走红黑树分支,找到要删除的结点
24                 if (p instanceof TreeNode)
25                     node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
26                 // 否则进行循环查找
27                 else {
28                     do {
29                         // 如果命中,则跳出循环
30                         if (e.hash == hash &&
31                                 ((k = e.key) == key ||
32                                         (key != null && key.equals(k)))) {
33                             node = e; // node结点保存命中的结点元素
34                             break;
35                         }
36                         // 如果未命中,则用p记录next结点,为后续删除做准备,p表示要删除节点的前一个节点,因为这里有e=e.next操作,与jdk1.7相同。
37                         p = e;
38                     } while ((e = e.next) != null);
39                 }
40             }
41             // 要删除结点不为null,&& 操作后:注意这里!matchValue,因为传入值为false,所以这里一直为true
42             if (node != null && (!matchValue || (v = node.value) == value ||
43                     (value != null && value.equals(v)))) {
44                 // 如果要删除节点为红黑树,则走红黑树分支
45                 if (node instanceof TreeNode)
46                     ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
47                 // 如果要删除节点与p相同,则说明是头结点,则直接将tab[index]位置指向node.next,这样就踢出了node元素,即删除
48                 else if (node == p)
49                     tab[index] = node.next;
50                 else // 如果不相同,则直接将p.next指向node.next,同样跳过了node,也就删除了。
51                     p.next = node.next;
52                 ++modCount; //操作记录+1,fail-fast机制
53                 --size;// 元素个数减1
54                 afterNodeRemoval(node);
55                 return node;
56             }
57         }
58         // 上述都未找到,则直接返回null
59         return null;
60     }

重点:

删除函数与jdk1.7中原理一样,都是通过操作一个结点元素进行删除,只是如果结点为红黑树,需走红黑树分支。

总结

这里比较粗略的分析了jdk1.8中HashMap的源码,它与jdk1.7中源码的原理大致相同,这里总结其重点:

#1.底层引入了红黑树数据结构,在添加元素时,如果table位置上的元素数量>=8时,则当前位置结点数据结构会转向红黑树;

当table位置上元素数量<=6时,数据结构又会转换成链表(在resize中,红黑树分支)。

#2.改变了hash算法,直接高16位与低16位做异或。

#3.resize函数中,在做再次hash时,用两条链表分散存储节点,并且避免了jdk1.7中的死循环情况。

#4.同样存在fail-fast机制。


by Shawn Chen,2019.03.09,晚。

posted @ 2019-03-09 22:26  developer_chan  阅读(494)  评论(1编辑  收藏  举报