JDK 1.8 HashMap扩容机制

我们首先来看利用无参构造函数创建HashMap如何扩容。首先创建一个无参构造出来的hashmap

 HashMap hashMap = new HashMap();

该构造函数源码如下:

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

此时,该构造函数只会设置默认的加载因子,即计算阈值threshold,默认为0.75大小。此时底层的table数组是指向null的

在这里插入图片描述
接下来调用put方法,往hashmap里面存一个键值对:

hashMap.put(1, 1);

由于底层的table是指向null的,所以需要肯定进行扩容。调用put方法,该方法的源码如下:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

该方法首先会调用hash(key),来计算我们传进来的key的hash值,我们看一下hash方法的源码:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

该方法接收一个Object类型的key,即所有类型的参数都可以接收。key分为两种情况计算:

  1. 如果key为null:计算结构为0。所以hashmap可以接收key为null的键值对,这是没问题的,因为在这里做了处理,如果key为null,哈希值为0
  2. 如果key不为null:计算结果为 (h = key.hashCode()) ^ (h >>> 16)。这里相当于 h^(h>>>16),其中h是key的hashCode方法返回值。把h与h的右移16为后进行异或操作,高16位保持不变,低16位会进行扰动。

我们重点来看一下第二种情况,key不为null时,HashMap如何优化hashCode函数,使得Hash值散列更分散。以一个具体的数为例,假设h = 123456789 。下面我用32位二进制数来表述h:

  • 原始的哈希值的二进制数:           0000 0111 0101 1011 1100 1101 0001 0101
  • 无符号右移16位后的的二进制数:0000 0000 0000 0000 0000 0111 0101 1011
  • 上面两个二进制数异或的结果:    0000 0111 0101 1011 1100 1010 0100 1110

由于是异或,即相同为0,不同为1,所以与0异或,结果就是本身。而无符号右移16位,说明高16位全是0,与高16为进行异或运算,说明结果不变,而低16位的值有0也有1,与低16位异或,结果就不一样了。

这种二进制级别的位操作能有效地分散哈希值,从而在哈希表中产生更好的键分布,减少潜在的哈希冲突。

接着往下走,调用 putVal(hash(key), key, value, false, true),putVal方法。下面是该方法的源码:

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;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    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;
    }

我们首先看该方法接收的参数:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)

接收key的哈希值hash,传进来的 key,value键值对,onlyIfAbsent 是false,evict是true。后面两个参数不重要,重点是前面三个参数:

  1. hash 经过计算得到kye的哈希值
  2. key
  3. value

进入函数,该方法创建四个局部变量:

Node<K,V>[] tab;
Node<K,V> p;
int n
int i;

HashMap底层的数组table就是Node<K,V>类型的,即Node数组,p就是一个node节点。
定义完局部变量,会进行该方法里面的第一个if条件判断:

if ((tab = table) == null || (n = tab.length) == 0)

该语句首先将成员变量table赋值给局部变量tab:

tab = table

然后判断tab是不是为null,或tab的长度是不是为0。这两种情况都对应一个结果:当前数组放不了传进来的键值对,需要进行扩容,所以该if条件里面的代码是:

n = (tab = resize()).length;

这里出现了扩容核心方法resize(),它在putVal方法里面出现了两次,我们总结这两次出现的条件:

  1. 第一次是if ((tab = table) == null || (n = tab.length) == 0) 判断为真,即第一次调用put方法会触发扩容。
  2. 第二次是if (++size > threshold)判断为真,即hashmap里面的元素个数大于阈值。

在这里插入图片描述

不同条件扩容结果是不一样的,但都调用了resize方法,我们看resize方法是如何控制的。
以下是resize方法的源码:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 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)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

这部分代码较长,其实可以划分为两部分,从if (oldTab != null) 处划分,上面是关于扩容后的容量计算,下面是关于扩容后元素迁移。
我们首先看第一部分:

Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 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)
        newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
else {               // zero initial threshold signifies using defaults
    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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;

首先定义了一大堆局部变量:

  • oldTab,旧表,oldTab = table
  • oldCap,旧表容量,(oldTab == null) ? 0 : oldTab.length;,因为可能是第一次构造,所以oldTab 可能为null,这里用三元表达式计算值。
  • oldThr,旧的阈值,oldThr = threshold
  • newCap,新的容量,newCap = 0
  • newThr,新的阈值,newThr = 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)
        newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
else {               // zero initial threshold signifies using defaults
    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);
}

我们先不关注判断语句里面的执行逻辑,我们看什么情况会执行到哪个逻辑代码块里。

if (oldCap > 0) {
	// 这里面执行的扩容逻辑表示不是第一次put扩容,而是后面达到阈值的扩容
}
else if (oldThr > 0) {
	// 运行到这里面需要oldCap = 0,表示是第一次put扩容,还需要oldThr > 0,即threshold > 0
	// 说明这里面是带参构造函数构造的map的扩容,因为带参构造函数才计算threshold
}
else {               // zero initial threshold signifies using defaults
    // 这里面是无参构造函数map,第一次调用put扩容时的处理逻辑
}
if (newThr == 0) {
    // 这里面是带参构造函数构造的map的扩容
}

为什么带参构造函数HashMap 的 threshold 不为0?
这需要去看源码了:

HashMap<Object, Object> hashMap = new HashMap<>(17);

这里构造了一个HashMap,使用带参构造函数,传入initCapacity

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

发现调用了另外一个构造函数,两个参数,需要传入加载因子,这里传入了DEFAULT_LOAD_FACTOR默认记载因子,0.75。继续进入该方法:

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

前面部分代码在对传入的initialCapacity 值做合法性判断,注意看最后一句代码:

this.threshold = tableSizeFor(initialCapacity);

使用tableSizeFor计算出一个不小于initialCapacity的2的幂次,然后返回给threshold ,注意看,这里threshold的值已经被计算出来了,但是table还是null,所以会出现oldTab = 0, oldThr > 0 的情况。
在这里插入图片描述
计算出了newCap之后,就需要新建一个数组了:

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;

此时如果是第一次扩容,就可以直接return newTab,如果不是第一次扩容,即oldTab不为null,里面还有数据,需要迁移到newTab

在这里插入图片描述
所以我们重点看一下,迁移这部分代码:

 if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
        Node<K,V> e;
        if ((e = oldTab[j]) != null) {
            oldTab[j] = null;
            if (e.next == null)
                newTab[e.hash & (newCap - 1)] = e;
            else if (e instanceof TreeNode)
                ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
            else { // preserve order
                Node<K,V> loHead = null, loTail = null;
                Node<K,V> hiHead = null, hiTail = null;
                Node<K,V> next;
                do {
                    next = e.next;
                    if ((e.hash & oldCap) == 0) {
                        if (loTail == null)
                            loHead = e;
                        else
                            loTail.next = e;
                        loTail = e;
                    }
                    else {
                        if (hiTail == null)
                            hiHead = e;
                        else
                            hiTail.next = e;
                        hiTail = e;
                    }
                } while ((e = next) != null);
                if (loTail != null) {
                    loTail.next = null;
                    newTab[j] = loHead;
                }
                if (hiTail != null) {
                    hiTail.next = null;
                    newTab[j + oldCap] = hiHead;
                }
            }
        }
    }
}

首先创建了一个for循环,全部的代码都在这段for循环里面:

for (int j = 0; j < oldCap; ++j)

这段for循环是变量oldTab所有槽位的元素。
定义一个局部变量,Node<K,V> e;,因为oldTab是Node类型的,e表示oldTabl每个槽位的元素。

if ((e = oldTab[j]) != null)

先让e = oldTab[j],第j个数组元素赋值给e,在判断e是否为空,如果为空,直接判断下一个槽位。如果不为空,将oldTab[j] 迁移到 newTab某个槽位处,所以这里是最关键的代码,我们看这个if里面的代码:

oldTab[j] = null;
if (e.next == null)
    newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
    Node<K,V> loHead = null, loTail = null;
    Node<K,V> hiHead = null, hiTail = null;
    Node<K,V> next;
    do {
        next = e.next;
        if ((e.hash & oldCap) == 0) {
            if (loTail == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
        }
        else {
            if (hiTail == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
        }
    } while ((e = next) != null);
    if (loTail != null) {
        loTail.next = null;
        newTab[j] = loHead;
    }
    if (hiTail != null) {
        hiTail.next = null;
        newTab[j + oldCap] = hiHead;
    }
}

首先让oldTab[j] = null; 此时e是执行这个位置原本指向的节点的
在这里插入图片描述
紧接着就是几个if-else将e指向的该Node节点分成了三种:

  1. e.next == null,就是单个节点,没有形成链表。
  2. e instanceof TreeNode,e不是单个节点,而是树的头节点
  3. else,e是链表的头节点

前面两种情况都很简单:

if (e.next == null)
    newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

对于第一种单个节点的情况,如源码所示,会用e的hash值与新容量-1的值进行与运算,即将e的hash值映射到newTab的索引上,直接然后newTab[计算出来的新索引] 指向该 Node节点。
对于第二种情况,e不是单个节点,而是树的头节点:

((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

需要调用该节点的split方法,将红黑树切分为两个链表,之后进行扩容操作。
第三种情况,e是链表的头节点,处理部分的源码:

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
    next = e.next;
    if ((e.hash & oldCap) == 0) {
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    else {
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);
if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
}
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
}

首先定义了一些局部变量:

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;

然后用一个do-while循环:

do {
    next = e.next;
    if ((e.hash & oldCap) == 0) {
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    else {
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);

在这里插入图片描述

在这里插入图片描述
第三种情况看似代码多,但比较简单。第三种情况是oldTab[j]位置的元素是一条链表,迁移到新数组上怎么做的?
通过遍历这条链上的节点e,然后根据下面的条件判断:

(e.hash & oldCap) == 0

把这条链上的所有节点分成了两块,e.hash & oldCap结果等于0,e.hash & oldCap结果不等于0。
在这里插入图片描述

// 定义两个链表的头和尾节点,分别用于低位链表和高位链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next; // 用于暂存当前节点的下一个节点

do {
    next = e.next; // 存储当前节点的下一个节点,以便迭代继续进行

    // 判断当前节点的哈希值与oldCap的关系,oldCap通常是数组的当前容量,用作重新哈希的分界线
    if ((e.hash & oldCap) == 0) {
        // 当前节点哈希值和oldCap按位与结果为0,表示当前节点应保持在原索引
        if (loTail == null)
            loHead = e; // 如果低位链表尾节点为空,即链表还未初始化,则当前节点为链表头节点
        else
            loTail.next = e; // 如果低位链表尾节点不为空,则将当前节点链接到链表尾部
        loTail = e; // 更新低位链表尾节点为当前节点
    }
    else {
        // 当前节点哈希值和oldCap按位与结果不为0,表示当前节点应移动到新索引位置
        if (hiTail == null)
            hiHead = e; // 如果高位链表尾节点为空,即链表还未初始化,则当前节点为链表头节点
        else
            hiTail.next = e; // 如果高位链表尾节点不为空,则将当前节点链接到链表尾部
        hiTail = e; // 更新高位链表尾节点为当前节点
    }
} while ((e = next) != null); // 迭代直到链表末尾

// 处理完所有节点后,设置链表尾部节点的next指针为null,表示链表结束
if (loTail != null) {
    loTail.next = null; // 低位链表尾节点next设为null
    newTab[j] = loHead; // 将低位链表头节点放入新表的对应位置
}
if (hiTail != null) {
    hiTail.next = null; // 高位链表尾节点next设为null
    newTab[j + oldCap] = hiHead; // 将高位链表头节点放入新表的偏移位置
}

我们在计算旧表中的节点e,在新表中的索引位置时,是用e.hash & (newCap - 1)计算的,为什么这里不这样计算?
HashMap的扩容过程中,节点在新表中的索引位置通常是通过e.hash & (newCap - 1)来计算的。这种计算方法确保了节点根据其哈希值被均匀分布到新的哈希表中。然而,在你提到的代码片段中,使用的是e.hash & oldCap,这里有一个特定的原因和上下文:

背景
在Java的HashMap中,容量(cap)始终是2的幂次方(例如,16, 32, 64等)。这样设计的主要原因是利用位运算来替代模运算,从而提高计算效率。当cap是2的幂次方时,newCap就是oldCap * 2

为什么使用e.hash & oldCap

  1. 掩码计算: 当使用e.hash & oldCap时,我们实际上是在利用oldCap作为一个掩码。在扩容操作中,oldCap是原哈希表的大小,而newCap是新哈希表的大小,且newCap = 2 * oldCap。在这种情况下,oldCap的值实际上是一个比新容量少一位的值(如,如果oldCap = 16,则oldCap - 1 = 15,二进制为1111)。

  2. 决定节点的分布:

    • 低位桶: 如果e.hash & oldCap的结果是0,这意味着在oldCap位上e.hash是0,因此e.hash的二进制表示中,从最低位到oldCap位的部分没有变化。这表明,节点在新表中的索引位置与在旧表中的位置相同,即index
    • 高位桶: 如果结果不是0,意味着在oldCap的位置e.hash是1,这表明节点在新表中的索引位置是它在旧表中的位置加上oldCap,即index + oldCap

总结
通过这种方式,split操作不需要对每个节点重新计算其在新表中的完整位置,而只需要决定它是留在原位置还是移动到原位置加上旧容量的位置。这简化了计算过程,并利用了已有的哈希值和位运算的性质,达到既快速又高效的重新分配节点的目的。

这种方法不仅保证了正确性,而且也使得扩容过程中的节点重新分配更为高效。

下面用一个具体的例子进行解释:

我们来通过一个具体的例子来解释这个过程。假设我们有一个HashMap,它的原始容量(oldCap)是16,这意味着它可以有从0到15的索引。现在这个HashMap需要扩容,新的容量(newCap)将会是32(即16的两倍)。

哈希表的容量和二进制表示

  • 旧容量 (oldCap): 16,二进制表示为 0001 0000
  • 新容量 (newCap): 32,二进制表示为 0010 0000

索引位置的决定
假设我们有一个键的哈希值为34,我们想确定它在旧表和新表中的位置。

  • 哈希值: 34,二进制表示为 0010 0010

1. 旧表中的位置
在旧表中,索引是通过hash & (oldCap - 1)得到的:

  • oldCap - 1 = 15,二进制表示为 0000 1111
  • 34 & 15 = 0010 0010 & 0000 1111 = 0000 0010 = 2
  • 因此,哈希值为34的键在旧表中的索引是2。

2. 新表中的位置
我们可以通过hash & (newCap - 1)计算新索引,但现在我们关心的是如何使用hash & oldCap来决定索引位置变化:

  • oldCap本身为16,二进制表示为0001 0000
  • 34 & 16 = 0010 0010 & 0001 0000 = 0000 0000
    • 因为结果是0,表示在oldCap位置上的位是0,这意味着哈希值为34的键在新表中的索引仍然是2(即保留在原位置,低位桶)。

如果哈希值是例如50,我们来看看它的处理:

  • 哈希值: 50,二进制表示为 0011 0010
  • 50 & 16 = 0011 0010 & 0001 0000 = 0001 0000
    • 因为结果不是0,表示在oldCap位置上的位是1,这意味着哈希值为50的键在新表中的索引是它原来的索引(旧表中的索引为2)加上oldCap(16),也就是18(即移动到新位置,高位桶)。

总结
这种通过hash & oldCap来判断元素应该留在原位置还是移动到新位置的方法是高效的,因为它利用了已经计算的哈希值,并且只涉及简单的位运算。这样可以在扩容时快速重新分配元素,而无需重新计算每个元素的哈希值。
在这里插入图片描述
最后这个split函数的源码如下:

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        if ((e.hash & bit) == 0) {
            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);
        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);
        }
    }
}

逻辑上和链条拆分类似,不过涉及到树的退化,有时间我在继续写吧。。。。

  • 20
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值