JDK1.8源码解析-HashMap (二)

JDK1.8源码解析-HashMap II

承接上一篇《JDK1.8源码解析-HashMap I》,本篇主要介绍关于Java8针对HashMap在数据结构上的优化,涉及如何将链表优化成红黑树以及对红黑树的操作。


1. 概述

在上一篇中我们基于put方法分析了HashMap的底层实现,并且知道当hash产生碰撞,HashMap会以链表存放这些keyHash相同的键值对,并且当链表长度大于等于TREEIFY_THRESHOLD时,会将该链表转换成红黑树;另一方面,put元素时,当哈希桶中本身已经是红黑树(通过首元素instanceof TreeNode判断),调用TreeNode的putTreeVal方法插入红黑树。

2. 源码解析

2.1 HashMap.treeifyBin

final void treeifyBin(Node[] tab, int hash) {
    int n, index; Node e;
    // 如果table的长度小于最小的转换容量阈值,则通过哈希桶的扩容来达解决hash碰撞的问题
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // e是桶中的首元素
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 准备树中的临时变量, 头hd, 尾tl
        TreeNode hd = null, tl = null;
        // 当
        do {
            // 将当前链表元素Node转为TreeNode
            TreeNode p = replacementTreeNode(e, null);
            // 第一次循环,将hd指向链表头元素的地址
            if (tl == null)
                hd = p;
            // 后续循环中,将当前TreeNode的prev指向前一循环的TreeNode
            // 前一循环的TreeNode的next指向当前TreeNode
            else {
                p.prev = tl;
                tl.next = p;
            }
            // tl指向当前TreeNode
            tl = p;
        } while ((e = e.next) != null);
        // 重新规整以tab[index]为头的红黑树
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

treeifyBin首先确定是否要通过红黑树转换方法解决hash碰撞,在桶容量小于MIN_TREEIFY_CAPACITY的情况下,通过resize()扩容解决碰撞,因为绝大多数hash碰撞主要由于hash&len-1相同导致(取模),而非真正的keyHash相同导致,
而当cap很小时,这种情况将更加普遍,因此通过扩容,可以将部分原来在同一链表中的元素扩散到其他桶位置(上一篇resize分析中已经说过,对于这种情况,部分键值对将分布到hash&len-1 + oldCap的位置)。接下来,如果确实需要转换红黑树,则先预处理链表中的元素,将他们都把类型转换为TreeNode并把他们链接起来,再通过treeify方法将他们树化,形成符合红黑树特性的一棵平衡树。下面,我们再看看treeify怎么实现红黑树建立算法。

final void treeify(Node[] tab) {
    TreeNode root = null;
    // 开始遍历从当前树节点开始直至叶节点
    for (TreeNode x = this, next; x != null; x = next) {
        // next指向当前节点的next
        next = (TreeNode)x.next;
        // 初始化left & right
        x.left = x.right = null;
        // 第一次循环, 将root指向当前节点x
        // 根节点的父节点为null
        // 根节点为黑色节点
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class kc = null;
            // 从根节点开始查找需要插入当前x的地方
            for (TreeNode p = root;;) {
                int dir, ph;
                K pk = p.key;
                // 父节点的hash大于本节点hash,本节点为left
                if ((ph = p.hash) > h)
                    dir = -1;
                // 父节点的hash小于本节点hash,本节点为righ
                else if (ph < h)
                    dir = 1;
                // 父节点hash等于本节点hash,包含两种情况
                else if ((kc == null &&
                          // k非Comparable<>类型
                          (kc = comparableClassFor(k)) == null) ||
                          // 或者k是Comparable,并且根据compare方法比较返回0
                         (dir = compareComparables(kc, k, pk)) == 0)
                    // 处理相等父hash=节点hash,处理规则只要做到保持一致即可,即当出现相同情况始终返回一致性的结果
                    dir = tieBreakOrder(k, pk);

                // 缓存p!=null赋给xp
                TreeNode xp = p;
                // 根据dir决定p指向left或者right
                // 如果p!=null,继续循环查找
                // 如果p==null,则p即为需要插入x的地方
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    // 当前x的父指向xp
                    x.parent = xp;
                    // dir<=0 x为left
                    if (dir <= 0)
                        xp.left = x;
                    else
                        // dir>0,x为right
                        xp.right = x;
                    // 插入后对可能破坏红黑树特性的情况进行重新平衡
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    // 经过balance(左旋or右旋),哈希桶中的头元素可能不是root
    // 因此需要将root移动到头部,该方法仅仅改变prev,next指针,对树形结构无影响
    moveRootToFront(tab, root);
}

treeify主要将root开始的链表元素根据红黑树规则进行树化,根据平衡树算法先插入二叉查找树,再根据平衡规则重新平衡该树,平衡后的结果可能由于左旋右旋使得原先的root不再是root,所以最后需要将root移动到哈希桶的头部,保证第一个元素是root,移动root的过程仅仅改变了相关元素的prev和next指针,不涉及left,right,parent,red等TreeNode属性,因此不会改变红黑树结构,因此,之后对以头元素为root的红黑树增删改查仍然保持O(logN)复杂度。这部分的最后,我们看下,平衡算法的实现:

static  TreeNode balanceInsertion(TreeNode root,
                 TreeNode x) {
    // 按照算法先将插入的节点变成红色
    x.red = true;
    // xp父节点
    // xpp祖父节点
    // xppl,xppr 祖父节点的子节点,叔叔节点
    for (TreeNode xp, xpp, xppl, xppr;;) {
        // 父节点为null, x为root
        // root为黑节点,返回x
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        // 父节点为黑,祖父节点为null,返回原root
        else if (!xp.red || (xpp = xp.parent) == null)        
            return root;
        // 父节点是祖父节点的左节点,且为红色
        if (xp == (xppl = xpp.left)) {
            // 祖父节点的右节点不为null,并且xppr是红节点,即叔叔节点为红色
            if ((xppr = xpp.right) != null && xppr.red) {
                // 叔叔节点设为黑色
                xppr.red = false;
                // 父节点设为红色
                xp.red = false;
                // 祖父节点设为红色
                xpp.red = true;
                // 设置当前节点为祖父
                x = xpp;
            }
            // 叔叔为黑色
            else {
                // 当前节点是父节点的右节点
                if (x == xp.right) {
                    // 以父节点为轴进行左旋
                    root = rotateLeft(root, x = xp);
                    // 如果当前节点(之前的父节点)的父节点为空,那么祖父节点为null
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    // 父节点设为黑色
                    xp.red = false;
                    if (xpp != null) {
                        // 祖父节点设为红色
                        xpp.red = true;
                        // 以祖父节点为轴进行右旋
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        // 父节点是祖父节点的右节点,算法相同,左右对换即可
        else {
            if (xppl != null && xppl.red) {
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                if (x == xp.left) {
                    root = rotateRight(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}

该部分完全是Java对CLRS中红黑树的算法的实现,同样的实现在java.util.TreeMap也有体现。这部分实现实际上与HashMap关系不那么密切了,因此不作详细分析了,有兴趣的读者可以参考红黑树(一)之 原理和算法详细介绍[1]的解释,也可以对比原著的伪代码进行学习。

2.1 HashMap.putTreeVal

final TreeNode putTreeVal(HashMap map, Node[] tab,
                               int h, K k, V v) {
    Class kc = null;
    boolean searched = false;
    // 获取当前节点的根节点
    TreeNode root = (parent != null) ? root() : this;
    // 遍历树查找需要插入元素的节点
    for (TreeNode p = root;;) {
        int dir, ph; K pk;
        // 如果当前节点hash>插入元素hash, 走左树
        if ((ph = p.hash) > h)
            dir = -1;
        // 如果当前节点hash<插入元素hash, 走右树        
        else if (ph < h)
            dir = 1;
        // 如果当前节点==带插入元素key(对象相等)或者当前节点Key equals 插入元素key
        // 返回当前节点,即返回需要插入元素在树中的位置
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        // 不符合上面对象相等或者equals返回false,但是keyhash相等
        else if ((kc == null &&
                  // k非Comparable<>类型
                  (kc = comparableClassFor(k)) == null) ||
                  // 或者k是Comparable,并且根据compare方法比较返回0
                 (dir = compareComparables(kc, k, pk)) == 0) {
            if (!searched) {
                TreeNode q, ch;
                searched = true;
                // 处理真正的hash碰撞
                // 不同的对象由于hash算法可能造成碰撞,这种碰撞与HashMap的hash映射算法无关
                // 例如:重写K类的hashCode()方法,使所有的对象都返回1,那么任意K的实例均会造成hash碰撞
                // 而这种碰撞是HashMap层面无法避免的,因此下面的分支就可以处理这种极端情况。
                // 当然现实场景中,如果hashCode利用MD5,SHA1等已经被证明存在碰撞的哈希算法, 也会进入该分支。
                // 分别查找左右树,查找相同的对象,如果找不到,说明确实没有相同对象存在                
                if (((ch = p.left) != null &&
                     (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                     (q = ch.find(h, k, kc)) != null))
                    return q;
            }
            // 通过约定的法则计算左右分支,该逻辑与treeify中相同。
            dir = tieBreakOrder(k, pk);
        }

        TreeNode xp = p;
        // 不断寻找树的分支直至叶节点,左节点或者右节点
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            // 缓存当前p的next
            Node xpn = xp.next;
            // 创建新的TreeNode,把原先p.next赋给x的next
            TreeNode x = map.newTreeNode(h, k, v, xpn);
            // 根据dir决定新节点应该是p的左节点还是右节点
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            // p.next指向新节点
            xp.next = x;
            // 新节点的父节点以及prev指向p
            x.parent = x.prev = xp;
            // 如果原先p.next(新节点的next)不为null,将其指向新节点
            if (xpn != null)
                ((TreeNode)xpn).prev = x;
            // 重排红黑树使之平棚,并将root移到哈希桶的首元素
            moveRootToFront(tab, balanceInsertion(root, x));
            // 返回null,表示新插入元素,并非找到可以替换的元素
            return null;
        }
    }
}
final TreeNode root() {
    for (TreeNode r = this, p;;) {
        // 不停寻找parent,直到parent==null,因为root.parent=null
        if ((p = r.parent) == null)
            return r;
        r = p;
    }
}

putTreeVal是红黑树版的putVal,核心是利用了红黑树本质上是一棵二叉查找树的本质,不断比较当前节点的hash和需要插入元素的hash从而确定插入元素在左树还是右树。产生Hash碰撞主要由于两个原因:1)不佳的equals方法和hashCode方法,或者只重写了其中的一个,产生equals方法和hashCode产生不一致的情况(Bad coding practice);2)hashCode本身已经存在漏洞(Compromised hash algorithm)。其余逻辑在 treeifybalanceInsertion 都已经或多或少涉及。

3. 小节

本篇继续介绍了HashMap在Java8中的红黑树实现,通过对如何将链表转化为红黑树进而分析了Java8如何实现平衡红黑树,最后回到上一篇遗留的问题,当哈希桶中的首元素是TreeNode时,怎么将元素插入树结构。经过了2篇博客的分析,大致介绍了Java8对hashmap的底层实现,聚焦在平时最常用的put方法,当然HashMap还有许多我们平时也经常用的方法,由于时间有限,将不展开分析了,其原理万变不离其宗都是基于对hash的映射–>决定桶的位置–>并根据数据结构(链表或红黑树)进行增删改查,而时间复杂度都依据underlying的数据结构。


以上

© 著作权归作者所有

引用

[1] 红黑树(一)之 原理和算法详细介绍 http://www.cnblogs.com/skywang12345/p/3245399.html

[2] 源码分析之HashMap的红黑树实现, https://www.jianshu.com/p/5b157d4be1ad

你可能感兴趣的:(JavaCore)