hashmap技术概览与扩容在Java7与Java8中的不同实现

hashmap技术概览:

  • 数组 + 链表的方式实现,当hash冲突的时候,会将新put值放到链表开头。
  • 初始化时会初始化容量(capacity)、加载因子(loadfactor)、阈值(threshold),其中threshold = capacity * loadfactor,缺省值分别是:12 = 16*0.75
  • count值大于等于阈值(threshold)时,会进行动态扩容,扩容时扩容成原来容量(capacity)的两倍,并对每个值进行重定位。
  • Java8后对链表进行了优化,如果链表长度超过8,会将链表变成红黑树

HashMap大部分的内容是比较好理解的,链表的实现是通过一个内部类Node实现的:

//实现自Map.Entry接口,包含当前值的hash值、key、value、next节点的指针
static class Node implements Map.Entry {
        final int hash;
        final K key;
        V value;
        Node next;
        //... 省略 ...
}

这里我们主要说下在动态扩容时hashmap是怎么实现的,Java8引入了红黑树,扩容方式也换了另一个方法,所以代码实现比Java7复杂了不止一倍,但本质差别不大,我们先从7的扩容代码resize()来理解扩容的重新定位是如何实现的:

void resize(int newCapacity) {   //传入新的容量  
    Entry[] oldTable = table;    //引用扩容前的Entry数组  
    int oldCapacity = oldTable.length;  
    if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)了  
        threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了  
        return;  
    }  

    Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组
    transfer(newTable);                         //!!将数据转移到新的Entry数组里,这里包含最重要的重新定位
    table = newTable;                           //HashMap的table属性引用新的Entry数组
    threshold = (int) (newCapacity * loadFactor);//修改阈值  
}
//遍历每个元素,按新的容量进行rehash,放到新的数组上
void transfer(Entry[] newTable) {  
    Entry[] src = table;                   //src引用了旧的Entry数组  
    int newCapacity = newTable.length;  
    for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组  
        Entry e = src[j];             //取得旧Entry数组的每个元素  
        if (e != null) {  
            src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)  
            do {  
                Entry next = e.next;  
                int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置  
                e.next = newTable[i]; //标记[1]  
                newTable[i] = e;      //将元素放在数组上  
                e = next;             //访问下一个Entry链上的元素  
            } while (e != null);  
        }  
    }  
}
//调用传入hash值和容量,如:indexFor(e.hash, newCapacity)
static int indexFor(int h, int length) {  
    return h & (length - 1);  //进行与操作,求出,这样比%求模快,这也是hashmap的容量都是2的次方的原因之一。
}  

其中的哈希桶数组table的size=2, 所以key = 3、7、5,put顺序依次为 5、7、3。在mod 2以后都冲突在table[1]这里了。这里假设负载因子 loadFactor=1,即当键值对的实际大小size 大于 table的实际大小时进行扩容。接下来的三个步骤是哈希桶数组 resize成4,然后所有的Node重新rehash的过程。

hashmap技术概览与扩容在Java7与Java8中的不同实现_第1张图片

我们再来看下JDK1.8做了哪些优化。经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,经过rehash之后,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。对应的就是下方的resize()的注释。
看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
hashmap技术概览与扩容在Java7与Java8中的不同实现_第2张图片
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
hashmap技术概览与扩容在Java7与Java8中的不同实现_第3张图片
因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图
hashmap技术概览与扩容在Java7与Java8中的不同实现_第4张图片
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。

Java8 resize()源码:

final Node[] resize() {
        Node[] oldTab = table;  //引用扩容前的node数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;  //旧的容量
        int oldThr = threshold;  //旧的阈值
        int newCap, newThr = 0;  //新的容量、阈值初始化为0
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {  //如果旧容量已经超过最大容量,让阈值也等于最大容量,以后不再扩容
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)  //如果旧容量翻倍没有超过最大值,且旧容量不小于初始化容量16,则翻倍
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold - 初始化容量设置为阈值
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults - 0的时候使用默认值初始化
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {  //计算新阈值,如果新容量或新阈值大于等于最大容量,则直接使用最大值作为阈值,不再扩容
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;  //设置新阈值
        @SuppressWarnings({"rawtypes","unchecked"})
            Node[] newTab = (Node[])new Node[newCap];
        table = newTab;  //创建新的数组,并引用
        //如果老的数组有数据,也就是是扩容而不是初始化,才执行下面的代码,否则初始化的到这里就可以结束了
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {  //轮询老数组所有数据
                Node e;  //以一个新的节点引用当前节点,然后释放原来的节点的引用
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)  //如果e没有next节点,证明这个节点上没有hash冲突,则直接把e的引用给到新的数组位置上
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode)e).split(this, newTab, j, oldCap);   //!!!如果是红黑树,则进行分裂
                    else { // preserve order
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node next;
                        do {  //从这条链表上第一个元素开始轮询,如果当前元素新增的bit是0,则放在当前这条链表上,如果是1,则放在"j+oldcap"这个位置上,生成“低位”和“高位”两个链表
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;  //元素是不断的加到尾部的,不会像1.7里面一样会倒序
                                loTail = e;  //新增的元素永远是尾元素
                            }
                            else {  //高位的链表与地位的链表处理逻辑一样,不断的把元素加到链表尾部
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {  //低位链表放到j这个索引的位置上
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) { //高位链表放到(j+oldCap)这个索引的位置上
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

从这里看,如果没有红黑树,其实1.7与1.8处理逻辑大同小异,区别主要还是在树节点的分裂((TreeNode)e).split() 这个方法上。

//resize时调用((TreeNode)e).split(this, newTab, j, oldCap);对树进行扩容或缩容,如果低于阈值会变成链表
/**
 * Splits nodes in a tree bin into lower and upper tree bins,
 * or untreeifies if now too small. Called only from resize;
 * see above discussion about split bits and indices.
 *
 * @param map the map
 * @param tab the table for recording bin heads
 * @param index the index of the table being split
 * @param bit the bit of hash to split on
 */
final void split(HashMap map, Node[] tab, int index, int bit) {
    TreeNode b = this;  //当前这个节点的引用,即这个索引上的树的根节点
    // Relink into lo and hi lists, preserving order
    TreeNode loHead = null, loTail = null;
    TreeNode hiHead = null, hiTail = null;
    int lc = 0, hc = 0;  //高位低位的初始树节点个数都设成0
    for (TreeNode e = b, next; e != null; e = next) {
        next = (TreeNode)e.next;
        e.next = null;
        if ((e.hash & bit) == 0) {  //bit=oldcap,这里判断新bit位是0还是1,如果是0就放在低位树上,如果是1就放在高位树上,这里先是一个双向链表
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            ++lc;
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }

    if (loHead != null) {
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);  //!!!如果低位的链表长度小于阈值6,则把树变成链表,并放到新数组中j索引位置
        else {
            tab[index] = loHead;
            if (hiHead != null) // (else is already treeified)如果高位树是空,即整个树没变化,那么树其实是不用重新调整的
                loHead.treeify(tab);
        }
    }
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}
//树转变为单向链表
final Node untreeify(HashMap map) {
    Node hd = null, tl = null;
    for (Node q = this; q != null; q = q.next) {
        Node p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    return hd;
}
//链表转换为红黑树,会根据红黑树特性进行平衡、左旋、右旋等
//TODO 这里不细讲了,后续我会写一篇博客专讲红黑树在这里的实现
final void treeify(Node[] tab) {
    TreeNode root = null;
    for (TreeNode x = this, next; x != null; x = next) {
        next = (TreeNode)x.next;
        x.left = x.right = null;
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class kc = null;
            for (TreeNode p = root;;) {
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    root = balanceInsertion(root, x);//对树进行平衡插入,里面包括左旋右旋等操作
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}

你可能感兴趣的:(java,java8)