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,晚。