在上一篇文章:红黑树(Red-Black Tree)解析 中我们了解了二叉查找树以及红黑树的概念和特性,并且对查找、插入和删除操作的实现源码进行了详细的剖析。其复杂的操作流程保证了红黑树的五条特性始终能够被满足,从而使得红黑树操作的时间复杂度为O(logN)。也正因为如此,Java的很多集合框架都引入了红黑树结构以提高性能。在JDK1.8中,我们常用的HashMap也成功傍身红黑树策马奔腾,下面就让我们一起看看HashMap是如何入手红黑树的。
这里我们依然从查找,插入和删除三个常用操作来进行分析。除去这三个操作之外还有一个地方与红黑树结构密切相关–resize扩容操作,关于HashMap的扩容我们在另一篇文章中有详述,这里就不再重复,有兴趣的童鞋请戳这里(Java中集合的扩容策略及实现)。
相关成员变量
首先,先介绍一下相关的成员变量
//哈希表中的数组,JDK 1.8之前存放各个链表的表头。1.8中由于引入了红黑树,则也有可能存的是树的根 transient Node<K,V>[] table; //树化阈值。JDK 1.8后HashMap对冲突处理做了优化,引入了红黑树。 //当桶中元素个数大于TREEIFY_THRESHOLD时,就需要用红黑树代替链表,以提高操作效率。此值必须大于2,并建议大于8 static final int TREEIFY_THRESHOLD = 8; //非树化阈值。在进行扩容操作时,桶中的元素可能会减少,这很好理解,因为在JDK1.7中, //每一个元素的位置需要通过key.hash和新的数组长度取模来重新计算,而1.8中则会直接将其分为两部分。 //并且在1.8中,对于已经是树形的桶,会做一个split操作(具体实现下面会说),在此过程中, //若剩下的树元素个数少于UNTREEIFY_THRESHOLD,则需要将其非树化,重新变回链表结构。 //此值应小于TREEIFY_THRESHOLD,且规定最大值为6 static final int UNTREEIFY_THRESHOLD = 6; //最小树化容量。当一个桶中的元素数量大于树化阈值并请求treeifyBin操作时, //table的容量不得小于4 * TREEIFY_THRESHOLD,否则的话在扩容过程中易发生冲突 static final int MIN_TREEIFY_CAPACITY = 64;
查找
HashMap中的查找是最常用到的API之一,调用方法为map.get(key),我们就从这个get方法看起:
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
可以看到这个方法调用了两个内部方法:hash和getNode,下面依次来看这两个方法:
//hash方法对传入的key进行了哈希值优化,具体做法为将key的哈希值h无符号右移16位之后与h本身按位异或, //相当于将h的高16位于低16位按位异或。这样做的原因在于一个对象的哈希值即使分布再松散,其低几位发生冲突的概率也较高, //而HashMap在计算index时正是用该方法的返回值与(length-1)按位与,结果就是哈希值的高位全归零,只保留低几位。 //这样一来,此处的散列值优化就显得尤为重要,它混合了原始哈希值的高位与低位,以此来加大低位的松散性。 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } /** * Implements Map.get and related methods * * @param hash hash for key //此处传入的就是上面hash方法的返回值,是经过优化的哈希值 * @param key the key * @return the node, or null if none */ final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //上文提到的计算index的方法:(n - 1) & hash,first是这个数组table中index下标处存放的对象 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node //如果first对象匹配成功,则直接返回 ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { //否则就要在index指向的链表或红黑树(如果有的话)中进行查找 if (first instanceof TreeNode) //如果first节点是TreeNode对象,则说明存在的是红黑树结构,这是我们今天要关注的重点 return ((TreeNode<K,V>)first).getTreeNode(hash, key); //否则的话就是一个普通的链表,则从头节点开始遍历查找 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
重点来了,这里关注下TreeNode.getTreeNode(hash, key)方法,这是1.8中引入红黑树后新增的操作,它对于HashMap在哈希冲突多发,产生长链表的情况下的查找效率有着极大的提升:
/** * Calls find for root node. */ //定位到树的根节点,并调用其find方法 final TreeNode<K,V> getTreeNode(int h, Object k) { return ((parent != null) ? root() : this).find(h, k, null); } final TreeNode<K,V> find(int h, Object k, Class<?> kc) { TreeNode<K,V> p = this; //p赋值为根节点,并从根节点开始遍历 do { int ph, dir; K pk; TreeNode<K,V> pl = p.left, pr = p.right, q; if ((ph = p.hash) > h) //查找的hash值h比当前节点p的hash值ph小 p = pl; //在p的左子树中继续查找 else if (ph < h) p = pr; //反之在p的右子树中继续查找 else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p; //若两节点hash值相等,且节点的key也相等,则匹配成功,返回p /****---- 下面的情况是节点p的hash值和h相等,但key不匹配,需继续在p的子树中寻找 ----****/ else if