HashMap源码(1.7与1.8对比)

一,1.7HashMap

1.属性

//默认初始化化容量,即16  
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

//最大容量,即2的30次方  
static final int MAXIMUM_CAPACITY = 1 << 30;  

//默认加载因子  
static final float DEFAULT_LOAD_FACTOR = 0.75f;  

//HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态  
static final Entry<?,?>[] EMPTY_TABLE = {};  

//空的存储实体  
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  

//HashMap的元素数目  
transient int size;  

//HashMap下次扩容是的阀值,threshold = 初始容量 * 加载因子
int threshold;  

//HashMap的加载因子  
final float loadFactor;  

//修改次数  
transient int modCount;  

//最大的扩容阈值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

//hash总值,默认为0,目的使hash更复杂,结果更散列 
transient int hashSeed = 0;  

2.构造方法

//容量取16,加载因子取0.75,构造HashMap  
public HashMap() {  
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);  
} 

//通过加载因子构造HashMap,容量取默认值,即16  
public HashMap(int initialCapacity) {  
    this(initialCapacity, DEFAULT_LOAD_FACTOR);  
} 
  
//通过初始容量和加载因子构造HashMap  
public HashMap(int initialCapacity, float loadFactor) {  
    if (initialCapacity < 0)//初始容量不能小于0
        throw new IllegalArgumentException("Illegal initial capacity: " +  
                                           initialCapacity);  
    if (initialCapacity > MAXIMUM_CAPACITY)//最大容量不能大于2的30次方 
        initialCapacity = MAXIMUM_CAPACITY;  
    if (loadFactor <= 0 || Float.isNaN(loadFactor))//加载因子必须为数字,并且不能小于0
        throw new IllegalArgumentException("Illegal load factor: " +  
                                           loadFactor);  

    this.loadFactor = loadFactor;  
    threshold = initialCapacity;  
    init();//这个实现为空,LinkedHashMap会使用 
} 

//通过其他Map来初始化HashMap,容量通过其他Map的size来计算,装载因子取0.75  
public HashMap(Map<? extends K, ? extends V> m) {  
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,  
                  DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);  
    inflateTable(threshold);//初始化HashMap底层的数组结构  
    putAllForCreate(m);//添加m中的元素  
}  

3.inflateTable(初始化HashMap)

  • 选取初始加载容量为:大于等于传入容量的2的幂次方。比如传入容量为10,那么实际初始化容量为16。
//初始化HashMap的底层数据结构  
private void inflateTable(int toSize) {
    int capacity = roundUpToPowerOf2(toSize);//选取合适的容量值
    
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//选取合适的threshold(扩容阀值)
    table = new Entry[capacity];//初始化底层数据结构
    initHashSeedAsNeeded(capacity);//选择合适的Hash总值,这里和虚拟机的配置有关
}  

//选择合适的容量值,容量值取大于等于最接近number的2的冪数  
private static int roundUpToPowerOf2(int number) {
    // 如果传入容量超出了最大值,则采用最大值。如果小于1,则容量使用1
    // 其余情况:容量 <= 最接近容量的2的幂数,比如 容量 10=>16,  16=>16
    /*
    * 假设传入值为10:
    *   首先,计算(number - 1) << 1,也就是(10-1) << 1 = 18,number - 1是为了防止特殊情况,比如number = 16
    *   然后调用Integer.highestOneBit(18),最终返回为16.
    *        二进制                十进制
    *       0001 0010               18
    *       0001 1111               31          首先,将最高位之后所有位转换为1
    *       0000 1111               15          右移一位之后,相减也就是 (11111)2 - (1111)2 => 31 - 15 = 16
    *       ----------------------------
    *       0000 1111               16
    * */
    return number >= MAXIMUM_CAPACITY  ?
            MAXIMUM_CAPACITY  : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}  

4.Hash总值

  • 使用hash总值的目的是:使原本的hash算法更加散列,需要加入虚拟机的配置。在jdk1.8的hashMap中,并没有该属性。
//选择合适的Hash总值,这里和虚拟机的配置有关 
final boolean initHashSeedAsNeeded(int capacity) {  
    boolean currentAltHashing = hashSeed != 0;  // hashSend默认值为0,所以该值默认为false
    boolean useAltHashing = sun.misc.VM.isBooted() &&  
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);  // 数组的实际容量 >= ALTERNATIVE_HASHING_THRESHOLD(默认为Integer.MAX,需要使用jvm参数修改)
    boolean switching = currentAltHashing ^ useAltHashing;  // currentAltHashing默认为false,所以只有useAltHashing为true才能触发下面的修改hash总值
    if (switching) {  
        hashSeed = useAltHashing  
            ? sun.misc.Hashing.randomHashSeed(this)  
            : 0;  
    }  
    return switching;  
}

5.Hash的计算方式

//计算key的Hash值,这里针对String类的Key优化了Hash函数
final int hash(Object k) {  
    int h = hashSeed;  //hash总值,初始值为0
    if (0 != h && k instanceof String) {//这里针对String优化了Hash函数,是否使用新的Hash函数和Hash因子有关  
        return sun.misc.Hashing.stringHash32((String) k);  
    }  
    /*
    * 这里将获取到的hashCode值>>>和^,是为了更好的平均分配定位hash桶位置索引,让高位的二进制数参与到定位hash桶位置的运算中。
    * */
    h ^= k.hashCode();  
    h ^= (h >>> 20) ^ (h >>> 12);  
    return h ^ (h >>> 7) ^ (h >>> 4);  
}  

6.定位Hash桶的计算方式

  • 定位Hash桶计算公式:h & (length-1)。
//根据Hash值和Hash表的大小选择合适的Hash桶  
static int indexFor(int h, int length) {
    /*
    * 返回需要保证两个条件:
    *       1.返回值小于length
    *       2.分配平均
    * 假设hash为17,传入容量为10,经过计算之后的容量为16,那么返回值就为 17 & (16 - 1)
    *       二进制             十进制
    *       0001 0001           17
    *      0000 1111           15
    *   &
    * -----------------------------
    *       0000 0001           1
    * */
    return h & (length-1);  
}

7.put方法

  • 如果底层数组为空,则会先初始化底层数组,默认容量为16。定位Hash桶位置,并且遍历该链表下的所有节点,如果有节点和插入节点的key相同,就覆盖该节点,并且返回旧的value,如果没有key相同,那么就会采用头插法,将该节点插入。
  • 如果key为空,则执行空的逻辑,说明HashMap可以存放key为null的元素,该元素默认存放在数组下标为0的链表中。
//添加元素  
public V put(K key, V value) {  
    if (table == EMPTY_TABLE) {//具体看:容量计算方法
        inflateTable(threshold);
    }  
    if (key == null)//如果key为空,则执行空的逻辑,说明HashMap可以存放key为null的元素
        return putForNullKey(value);  
    int hash = hash(key);//获取key的Hash值  
    int i = indexFor(hash, table.length);//定位Hash桶  
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {//定位到hash桶的链表上,是否有key相同的元素,如果有返回并修改旧值
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//hash相同,并且key相同
            V oldValue = e.value;  
            e.value = value;//新值覆盖旧值
            e.recordAccess(this);//调用value的回调函数,其实这个函数也为空实现  
            return oldValue;//返回旧值
        }  
    }  

    modCount++;//更新修改次数  
    addEntry(hash, key, value, i);//添加到对应Hash桶的链接表中,头插法
    return null; //如果该key以前没有被添加过,则返回null
} 

8.ModCount属性

  • ModCount:表示修改次数,这是一种快速失败的机制,假设有一个线程在遍历hashmap,另一个线程在put或者remove,当它发现modCount != expectedModCount就会快速抛出异常ConcurrentModificationException,结束任务。
HashIterator() {  
    expectedModCount = modCount;//构造函数会传入modCount的初始值,如果之后的修改删除modCount != expectedModCount会抛出异常ConcurrentModificationException
    if (size > 0) { //目前有元素  
        Entry[] t = table;  
        while (index < t.length && (next = t[index++]) == null)//寻找第一个不为空的Hash桶  
            ;  
    }  
} 
//返回下一个元素  
final Entry<K,V> nextEntry() {  
    if (modCount != expectedModCount)//迭代期间有修改,则抛出异常  
        throw new ConcurrentModificationException();  
    Entry<K,V> e = next;//从next开始遍历  
    if (e == null)  
        throw new NoSuchElementException();  

    if ((next = e.next) == null) {//如果为空,则找到下一个不为空的Hash桶  
        Entry[] t = table;  
        while (index < t.length && (next = t[index++]) == null)  
            ;  
    }  
    current = e;//返回next的值  
    return e;  
}  
//删除元素  
public void remove() {  
    if (current == null)  
        throw new IllegalStateException();  
    if (modCount != expectedModCount)  
        throw new ConcurrentModificationException();  
    Object k = current.key;  
    current = null;  
    HashMap.this.removeEntryForKey(k);//调用父类删除元素  
    expectedModCount = modCount;//修改并发修改次数  
} 

9.Hash桶的扩容

9.1 扩容源码

//添加Entry  
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {//判断是否需要扩容,存放元素(包含链表)大于等于阈值,并且定位hash桶首个元素不为null
        resize(2 * table.length);//按2倍扩容,执行扩容
        hash = (null != key) ? hash(key) : 0;//计算新插入元素的hash桶位置。  
        bucketIndex = indexFor(hash, table.length);
    }  

    createEntry(hash, key, value, bucketIndex);//创建元素  
} 
//执行扩容
void resize(int newCapacity) {  
    Entry[] oldTable = table;//老的数据  
    int oldCapacity = oldTable.length;//获取老的容量值  
    if (oldCapacity == MAXIMUM_CAPACITY) {//老的容量值已经到了最大容量值  
        threshold = Integer.MAX_VALUE;//修改扩容阀值  
        return;  
    }  
    //新的结构  
    Entry[] newTable = new Entry[newCapacity];  
    transfer(newTable, initHashSeedAsNeeded(newCapacity));//将老的数据拷贝到新的结构中。initHashSeedAsNeeded为是否使用hash总值,与虚拟机配置有关,默认为false
    table = newTable;//修改HashMap的底层数组  
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//修改阀值  
} 
//将老的数据拷贝到新的结构中  
void transfer(Entry[] newTable, boolean rehash) {  
    int newCapacity = newTable.length;//新的容量  
    for (Entry<K,V> e : table) {  
        while(null != e) {  
            Entry<K,V> next = e.next;  
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);  
            }  
            int i = indexFor(e.hash, newCapacity);//定位Hash桶,计算的结果可能为:初始位置 或者 初始位置 + 扩容量
            e.next = newTable[i];//扩容插入也会采用头插法,与旧的hash桶相比链表的顺序可能会被颠倒,并且分散到 初始位置 和 初始位置 + 扩容量两个位置中
            newTable[i] = e;  
            e = next;  
        }  
    }  
} 

9.2 扩容的条件

  • 扩容的条件为:((size >= threshold) && (null != table[bucketIndex])) ,即存放元素(包含链表)大于扩容阈值,并且定位hash桶位置首个元素不为null。

9.3 扩容的运行机制

  • hash桶的扩容默认为原来hash桶的2倍进行扩容,扩容之后旧的元素会被拷贝到新的hash桶中,复制到的位置会为两种,分别为:初始位置(旧的位置) 或者 初始位置 + 扩容量(也就是原来的hash桶的长度),复制也会采用头插法。所以,在复制之后,新的hash桶与旧的hash桶链表的顺序会相反。在多线程的情况下,可能会出现环形链表的情况,导致数据不安全。

9.4 扩容的优势

  • 扩容不仅是提升数组长度,通过扩容可以将原来的链表变短,因为链表的hash会重新计算(hash & length -1),计算结果会出现两种情况,所以原本链表可能会被分配到两个位置,从而提升查找效率。

9.5 如何避免HashMap扩容

  • 假设需要固定的30个容量(需要的容量为30)。
    • 那么只需要让扩容阈值永远达不到30即可(扩容阈值 = 实际容量 * 加载因子)。
    • 那么构造传入容量32,通过计算的实际容量也就为32,加载因子为1,那么hashmap只有达到32(扩容阈值)才会扩容,需要30个容量的话,就不会扩容。

二,1.8HashMap

1.put方法

  • 执行流程:
    1. 判断数组是否为空,为空则先进行初始化数组,在jdk1.8中初始化数组的方法为resize()。
    2. 定位插入元素的下标位置,定位之后可能会走以下情况:
      1. 定位数组下标位置,第一个元素为空,则直接插入。
      2. 定位数组下标位置,第一个元素不为空,说明该下标位置已经变为了单向链表或者红黑树。
        1. 首先会判断,下标位置第一个元素是否与插入元素key相同,如果相同那么返回旧值,执行覆盖。
        2. 下标位置第一个元素与插入元素的key不相同,那么会分为以下两种逻辑:
          1. 节点类型属于树形节点,执行putTreeVal(),如果插入元素与树形节点的key相同,则返回旧值,执行覆盖。
          2. 节点类型属于普通节点,执行普通插入(尾插法),当下标位置下的元素个数超过8,那么在插入第9个元素时,链表会执行树化treeifyBin(),如果插入元素与单向链表节点的key相同,则返回旧值,执行覆盖。
    3. 如果运行到这一步,说明插入元素为第一次插入,没有与插入元素相同的key。那么会将修改次数+1。
    4. 判断HashMap中元素的数量是否大于扩容阈值,如果大于执行扩容。
    5. 返回null。
 public V put(K key, V value) {
     return putVal(hash(key), key, value, false, true);
 }
 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)// 定位数组下标位置,并且第一个元素为null
        tab[i] = newNode(hash, key, value, null);
    else {// 第一个元素不为null,说明下面已经变为链表或者红黑树
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))// hash相同,并且key相同,说明是插入节点和第一个节点是key相同
            e = p;
        else if (p instanceof TreeNode)// 说明节点个数已经大于8,链表变为红黑树
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {// e第一次循环为第二个节点
                    p.next = newNode(hash, key, value, null);// 插入节点
                    if (binCount >= TREEIFY_THRESHOLD - 1) // 当前链表长度大于8,也就是插入第九个节点时候,调用treeifyBin
                        treeifyBin(tab, hash);// tab为hashMap维护的数组,hash为当前元素的hash
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))// hash相同,并且key相同,说明是插入节点和e节点的key相同
                    break;
                p = e;// 继续循环,指针指向p.next
            }
        }
        if (e != null) { // 如果e不为null,说明当前节点已经插入过,返回旧值,执行覆盖
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)// 插入之后hashMap的大小大于扩容阈值
        resize();
    afterNodeInsertion(evict);
    return null;
}

2.resize方法

  • 1.7与1.8resize方法的不同点:
    • 1.7的resize方法只负责扩容,不负责初始化数组。1.8既要负责扩容,还要负责数组的初始化。
    • 1.7旧数组上的链表迁移节点使用的是头插法。1.8中链表迁移节点使用的是尾插法。
    • 1.7扩容的条件存放元素(包含链表)大于扩容阈值,并且定位hash桶位置首个元素不为null。1.8扩容条件为存放元素大于扩容阈值。
  • 扩容的优势依然是使原来的链表或者红黑树节点上的元素变少,变得散列,提升查找效率。
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;// 阈值变为最大值,默认阈值为 数组长度*0.75
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&// 扩容的容量为原来数组的2倍,新的容量小于最大值 并且 旧的容量大于16
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // 新的阈值为:旧的阈值 * 2
    }
    else if (oldThr > 0) // 如果旧数组的容量为0, 旧数组的阈值大于0, 是因为初始容量被放入阈值,则将新数组的容量设置为旧数组的阈值
        newCap = oldThr;
    else {               // 旧数组的容量为0, 旧数组的阈值为0,这种情况是没有传初始容量的new方法创建的空表,将阈值和容量设置为默认值
        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)// 如果e.next为null,说明当前链表只有一个节点
                    newTab[e.hash & (newCap - 1)] = e;// 将唯一的一个节点插入新的数组中
                else if (e instanceof TreeNode)// 对应数组元素是红黑树的处理
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 对应数组元素是链表的处理
                    Node<K,V> loHead = null, loTail = null;// lower链表,该链表对应的下标与旧数组一致
                    Node<K,V> hiHead = null, hiTail = null;// hieght链表,该链表对应的下标为旧数组下标 + 旧数组长度
                    Node<K,V> next;
                    do {// 执行与运算,将原本的链表进行拆分
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {// 运算结果为0,那么对应节点插入到lower链表,尾插法
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {// 运算结果为1,那么对应节点插入到hieght链表
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {// lower链表不为null
                        loTail.next = null;
                        newTab[j] = loHead;// 将lower链表插入新数组,索引与旧数组一致
                    }
                    if (hiTail != null) {// height链表不为null
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;// 将height链表插入新数组,索引为旧数组索引 + 旧的数组长度
                    }
                }
            }
        }
    }
    return newTab;
}

3.treeifyBin方法

3.1 源码

// tab参数是hashMap维护的数组,hash是插入节点的hash值
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)// 如果数组为null或者数组长度小于64,那么会执行扩容,并不会将链表转换为红黑树
        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);// 将普通节点替换为树形节点
            if (tl == null)
                hd = p;// 第一个树形节点为hd
            else {// 将树形节点转换为双向链表
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)// 如果第一个树形节点不为null,说明当前链表长度已经大于8,并且数组长度为32
            hd.treeify(tab);// 将链表转换为红黑树
    }
}

3.2 转换规则和作用

  • 转化规则:
    1. 如果数组为null,或者数组长度小于64,即使该链表上的节点已经大于8个,也不会将链表转换为红黑树。
    2. 只有数组索引位置不为null,并且数组长度大于等于64,链表长度大于8,才会将链表转换为红黑树。
  • 作用:判断是否将链表转化为红黑树。

3.3 链表转换为红黑树的条件

  • 链表转换为红黑树必须满足以下三个条件:
    1. 数组不为null。
    2. 链表上的节点数量大于8。
    3. 数组长度大于64。

3.4 转换过程

  • 单向链表 --> 双向链表 --> 红黑树

4.treeify方法

4.1 源码

final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {// 遍历整个链表
        next = (TreeNode<K,V>)x.next;// x表示当前节点,next表示x的下一个节点
        x.left = x.right = null;
        if (root == null) {// 链表的第一个节点,默认为红黑树的根节点(根节点会根据条件发生变化)
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {// 当存在根节点时,根据大小判断其父节点
            K k = x.key;// x表示链表的元素
            int h = x.hash;
            Class<?> kc = null;
            for (TreeNode<K,V> p = root;;) {// 从根节点开始遍历寻找当前链表节点的父节点
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h)// ph表示父节点的hash值,h表示当前链表节点
                    dir = -1;// 该链表节点在当前父节点的左子树上
                else if (ph < h)
                    dir = 1;// 该链表节点在当前父节点的右子树上
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) || // comparableClassFor:如果该key实现Comparable接口返回该key的类,否则返回null
                         (dir = compareComparables(kc, k, pk)) == 0)// compareComparables:因为如果执行此方法,表示comparableClassFor已经返回了key的类对象,执行compareTo比较key大小
                    dir = tieBreakOrder(k, pk);// 如果(kc == null && (kc = comparableClassFor(k)) == null) 为true执行此方法,所以tieBreakOrder和compareComparables只会执行一个
                            // tieBreakOrder先回比较类名的长度,如果相等,那么会执行System.identityHashCode(a)比较hashcode
                TreeNode<K,V> 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);// 将数组最初的链表替换为红黑树
}

4.2 作用

  • 将链表调整为红黑树,并且调用moveRootToFront将红黑树的根节点调整为数组上的第一个元素。

4.2 定位父节点的过程

  1. 首先,判断的是当前节点与父节点的hash。
  2. 使用hash判断相等后,会查看该类的key是否实现Comparable接口:
    1. 实现Comparable接口:调用compareTo执行判断。
    2. 未实现Comparable接口,会执行3的判断。
  3. 调用tieBreakOrder判断:先会比较类名的长度,如果相等,那么会执行System.identityHashCode比较hashcode。

5.untreeif方法

5.1 源码

final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = this; q != null; q = q.next) {
        Node<K,V> p = map.replacementNode(q, null);// 将TreeNode转换为Node,所有红黑树的属性就不存在了
        if (tl == null)
            hd = p;// 头节点
        else
            tl.next = p;
        tl = p;
    }
    return hd;// 返回链表的第一个节点
}

5.2 作用

  • 作用:hashMap扩容之后,数组位置为红黑树的节点可能会被拆分到两个位置(原位置,原位置+扩容量),对应位置的节点小于等于6(修复阈值)时,调用此方法将最初的红黑树转换为单向链表。

6.moveRootToFront方法

6.1 源码

// tab表示hashMap维护的数组,root表示红黑树的根节点
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
    int n;
    if (root != null && tab != null && (n = tab.length) > 0) {
        int index = (n - 1) & root.hash;// 定位红黑树对应的数组下标
        TreeNode<K,V> first = (TreeNode<K,V>)tab[index];// 数组上的元素,也就是链表的第一个节点
        if (root != first) {// 如果红黑树的根节点和链表上的第一个节点不一致
            Node<K,V> rn;
            tab[index] = root;// 红黑树的根节点覆盖链表上的第一个节点
            // 以下代码的逻辑是:将根节点放到链表的第一个节点上,并修复双向链表
            TreeNode<K,V> rp = root.prev;// 根节点的上一个节点
            if ((rn = root.next) != null)// 根节点的下一个节点不为null
                ((TreeNode<K,V>)rn).prev = rp;
            if (rp != null)
                rp.next = rn;
            if (first != null)
                first.prev = root;
            root.next = first;// root成为双向链表的第一个节点,next指向原来的第一个节点
            root.prev = null;// root成为双向链表的第一个节点,所以prev为null
        }
        assert checkInvariants(root);// 检查生成的红黑树是否有问题
    }
}

6.2 作用和修复过程

  • 作用:将数组上的节点替换为红黑树的根节点,并且修复双向链表。
  • 修复过程:
    1. 将红黑树的根节点放到双向链表的第一个节点。
    2. 修复双向链表。
    3. 检查生成的红黑树是否有问题。

7.Split方法

7.1 源码

// map为hashMap对象,tab为新的数组,index为对应数组下标,bit为旧的数组长度
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;// lower链表长度
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;// height链表长度
        }
    }

    if (loHead != null) {// 如果lower链表不为null
        if (lc <= UNTREEIFY_THRESHOLD)// lower链表长度 <= 恢复链表阈值(6)
            tab[index] = loHead.untreeify(map);// 将红黑树转换为链表,并插入到与旧数组一样的索引位置
        else {
            tab[index] = loHead;
            if (hiHead != null) // 如果heigth链表中有节点
                loHead.treeify(tab);// 修复lower链表对应的红黑树,因为hiHead中有节点,说明原来的红黑树已经发生了改变
        }
    }
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)// heigth链表长度 <= 恢复链表阈值(6)
            tab[index + bit] = hiHead.untreeify(map);// 将红黑树转换为链表,索引为旧数组索引 + 旧的数组长度
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)// 如果lower链表中有节点
                hiHead.treeify(tab);// 修复heigth链表对应的红黑树,因为hiHead中有节点,说明原来的红黑树已经发生了改变
        }
    }
}

7.2 作用

  • 作用:hashMap扩容后,将红黑树节点进行拆分,对拆分的两个链表进行恢复链表或者修复红黑树(根据修复阈值判断,默认为6),最终将两个链表分别插入到扩容后的数组中。
    • 如果拆分后的链表长度小于等于6:调用untreeify将红黑树转换为链表。
    • 如果拆分后的链表长度大于6:调用treeify修复红黑树。
  • 注意:untreeify和treeify并不是一定会被调用的,根据拆分后的情况决定是否调用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值