HashMap笔记-JDK1.8

之前写过一篇HashMap笔记,不过是JDK1.7(含)之前HashMap,JDK在8后对HashMap做了很多的优化,在key冲突情况下的HashMap中的元素会形成一条链表,如果冲突key过多,则链表会越长,查询会变成线性时间复杂度O(log(n)),JDK8加入了红黑树来优化这种情况,所以也进行了再次研究下源码。

我们先从put方法入手。

JDK8的HashMap在插入元素时

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //1找到key匹配的节点
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //2没有找到key,但槽位上是树节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //3不是树节点,则还是链表结构
        else {
            for (int binCount = 0; ; ++binCount) { //A处
                //4依次遍历链表的节点
                if ((e = p.next) == null) {
                    //6遍历完链表,key不存在,则新建一个节点
                    p.next = newNode(hash, key, value, null);
                    //7加上新的节点,如果链表的节点数量大于8,则将链表转换成树结构。
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //5在链表上找到key
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

在源码的“A处”,新加一个节点,如果加上新的节点,节点的链表长度超过8个(TREEIFY_THRESHOLD值为8),则会尝试将链表转成树结构(使用treeifyBin方法去尝试转成树)

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //A1处
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null); //B处
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null) //C处
            hd.treeify(tab);
    }
}

图解:


为什么我在上面说是尝试转成树结构呢?

从treeifyBin方法的“A1”处的源码可以看到,当整个HashMap的Node<K,V>[] table的长度还没达到最小转换树容量(MIN_TREEIFY_CAPACITY = 64)时,只会对整个HashMap进行扩容resize不会将链表转成树;当达到了MIN_TREEIFY_CAPACITY的容量后,才会对table[index = (n - 1) & hash]的链表进行转换树结构的操作,
MIN_TREEIFY_CAPACITY的值,刚好是4 * TREEIFY_THRESHOLD(值是8),刚好是链表转树节点数的4倍,这里设置了4倍,如果设置过小的话,再次resize后可能节点计算得到的槽位冲突,过大的话,可能导致一个槽位的元素过多;

在源码的“B处”生成一个树节点,只是纯粹一个树节点,节点与节点之前形成双向链表,还没有生成树的左子树和右子树。
在“B处”这里的do ... while语句块主要是将链表里的节点转换成单独的树节点,树节点之间先用prev和next和链接起来,树节点结构:

// For treeifyBin
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}
TreeNode的结构定义:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
}
下面才是将节点组装成一棵树,
在“B处”已经将节点全部换成树节点后,“C处”开始将链表里的单独的树节点连接成一棵树(使用TreeNode类的treeify方法)

final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) { //D1处
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (root == null) {
            x.parent = null;
            x.red = false; //根节点是黑色节点
            root = x; //D2,链表的首节点设为树的根节点
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            for (TreeNode<K,V> p = root;;) { //D9
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h) //D3
                    dir = -1;
                else if (ph < h)  //D4
                    dir = 1;
                else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) //D5
                    dir = tieBreakOrder(k, pk); //D6

                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) { //D7
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    root = balanceInsertion(root, x); //D8
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}
在“D1处”,TreeNode<K,V> x = this,是说明从链表的头节点开始来组装树的,“x = next”,则是循环下一个节点。
在“D2处”,链表的头节点作为树的根节点,根节点是黑色的,接着循环下一个节点,并跟根节点做比较。
从D3-D5,则是节点和根节点进行比较,
其中D3,D4是比较节点的hash值,如果hash值无法区分大小,则继续看"D5",
在“D5”,看节点key是实现了Comparable接口,即是comparableClassFor(k)方法获取Comparable Class,
如果节点key实现了Comparable接口,则调用Comparable的compareTo(Object o)方法和根节点key比较,这一过程是在compareComparables(kc, k, pk)方法完成;
如果节点key没实现Comparable接口或者调用了compareTo还是无法区别大小,则看"D6";
在“D6”,tieBreakOrder方法,会先根据节点key和和根节点key的class name来比较大小,也就是String类的compareTo方法(String实现了compareTo方法),如果还不能区分大小,最终会使用System.identityHashCode(Object x)方法(这个方法是根据对象在内存中的地址来计算出一个值)来比较大小。

总结来说,经过D3或D4或D5或D6之后,肯定能分出当前节点和根节点对比大小:
hash值->Comparable接口->key Class name->System.identityHashCode()
通过以上顺序来得到dir值。

所以在上面这么复杂的过程,就是为了比较节点和根节点的大小来决定先后顺序,不能使两者相等,从而组装成树。
在“D7处”,如果节点比根节点小,则往根节点的左子树p=p.left找,否则往根节点的右子树p=p.right找,左(右)节点不为空时则回到循环D9,当前节点继续和左(右)节点对比,直接左(右)节点为空null时,即当前节点作为新的左(右)子树,并且当前节点的父节点指向旧的左(右)节点。

总结来说,当前节点和已经排在红黑树里的节点逐一比对,然后插入到合适的位置。

其中在“D8处”,当一个节点插入红黑树作为新节点时,需要根据红黑树的规则处理整棵树的平衡,因为红黑树的查找时间比一般二叉树更优。下面看看balanceInsertion方法,是插入元素时如何处理红黑树平衡的。

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
    x.red = true; //E1,新插入红黑树的节点(根节点除外)默认是红色
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        if ((xp = x.parent) == null) { //E2,找到节点的父节点
            x.red = false;
            return x;
        }
        else if (!xp.red || (xpp = xp.parent) == null) //E3,父节点是黑色,否则,父节点的父节点为null,即父节点是根节点
            return root;
        if (xp == (xppl = xpp.left)) { //E4,父节点是左节点
            if ((xppr = xpp.right) != null && xppr.red) { //E5,父节点的兄弟节点(右节点或者说是叔父节点)存在,并且是红色的
                xppr.red = false;  //叔父节点红色改成黑色
                xp.red = false;    //父节点点红色改成黑色
                xpp.red = true;    //父节点的父节点改成红色
                x = xpp;  //指向父节点的父节点
            }
            else { //E6,叔父节点为null,或者叔父节点是黑色的
                if (x == xp.right) { //E7,当前节点是右节点
                    root = rotateLeft(root, x = xp); //以父节点为支点,左旋
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) { //E8,有父节点
                    xp.red = false; //改颜色,父节点改为黑色
                    if (xpp != null) { //E9,父节点也有父节点
                        xpp.red = true; //改颜色,父节点的父节点改为红色
                        root = rotateRight(root, xpp); //E10,以父节点的父节点为支点,右旋
                    }
                }
            }
        }
        else { //E11,父节点是右节点
            if (xppl != null && xppl.red) { //E12,叔父节点存在,并且是红色的
                xppl.red = false;  //改变颜色,叔节点改为黑色
                xp.red = false;    //父节点改为黑色
                xpp.red = true;    //父节点的父节点改为红色
                x = xpp;  //指向父节点的父节点
            }
            else { //E13,叔父节点不存在,或者是黑色的
                if (x == xp.left) { //E14,当前节点是左节点
                    root = rotateRight(root, x = xp); //以父节点为支点,右旋
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) { //E15,有父节点
                    xp.red = false; //改颜色,父节点改为黑色
                    if (xpp != null) { //E16,父节点也有父节点
                        xpp.red = true;   //改颜色,父节点的父节点改为红色
                        root = rotateLeft(root, xpp);  //以父节点的父节点为支点,左旋
                    }
                }
            }
        }
    }
}
源码“E1”处,新插入红黑树的节点默认是红色的(新插入树的根节点是黑色除外),

左旋源码

static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) {
    TreeNode<K,V> r, pp, rl;
    if (p != null && (r = p.right) != null) {      //p节点有右节点r
        if ((rl = p.right = r.left) != null)       //右节点r的左节点变为p节点的右节点rl
            rl.parent = p;                         //设置rl的父节点为p节点
        if ((pp = r.parent = p.parent) == null)    //如果p的父节点为null,那么p就是树的根节点
            (root = r).red = false;                //p的右节点r变为树新的根节点,并且根据红黑树的规则将r节点的颜色改为黑色
        else if (pp.left == p)                     //如果p是左节点,则p的右节点r变为p的父节点的左节点,即r取代p的位置
            pp.left = r;
        else
            pp.right = r;                          //否则p是右节点,则p的右节点r变为p的父节点的右节点
        r.left = p;
        p.parent = r;
    }
    return root;
}
左旋看下图:


右旋源码

static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) {
    TreeNode<K,V> l, pp, lr;
    if (p != null && (l = p.left) != null) {      //p节点有左节点l
        if ((lr = p.left = l.right) != null)      //左节点l的右节点变为p节点的左节点lr
            lr.parent = p;                        //设置lr的父节点为p节点
        if ((pp = l.parent = p.parent) == null)   //如果p的父节点为null,那么p就是树的根节点
            (root = l).red = false;               //p的左节点l变为树新的根节点,并且根据红黑树的规则将l节点的颜色改为黑色
        else if (pp.right == p)                   //如果p是右节点,则p的左节点l变为p的父节点的右节点,即l取代p的位置
            pp.right = l;
        else
            pp.left = l;                          //否则p是左节点,则p的左节点l变为p的父节点的左节点
        l.right = p;
        p.parent = l;
    }
    return root;
}
右旋看下图:


JDK1.8的hash计算
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
h >>> 16,即是hash值的高16位往右移到最低的16位,最高16变0;
(h = key.hashCode()) ^ (h >>> 16),即是高16位和低16位异或运算;异或运算:相同为0,不同为1.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值