HashMap小探(二)—存、取、扩容

hashmap的结构

Snip20180615_8.png

如上图所示,hashmap的组成有两部分,数组 + 链表,不过在jdk1.8之后,变成了数组 + 链表 + 树,树为红黑树。

在上图中,数组中存储的是 一个链表的头结点,即first,节点要放在哪个下标,需要通过hash(key.hashcode) % tab.length (对数据取模,不过实际运算时采用&按位与运算) 来计算。

数组的每个下标成员都是一个链表的头节点,链表元素之间维护一个指针,指向下一个元素。链表中的元素key值对应的hash(key.hashcode) % tab.length 都是相同的,也就是有相同的数组下标。

在jdk1.8之前,每个index对应的中都是一个单向链表,在极端情况下,如果每个key对应的index都相同,hashmap将会变成类似上图所示,index从1到7的数据都是空的,所有数据都在index=0处的链表中。假设此时有8个元素,如果对hashmap get数据,时间复杂度将从O(1)变成O(n).也就是从数组中根据下标直接取,变成了需要根据链表一个一个遍历查找。

而在jdk1.8之后,这部分实现做了优化,详见下文。

补充下数组和链表的特性:


数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费。

数组是静态分配内存,并且在内存中连续。
数组利用下标定位,时间复杂度为O(1)
数组插入或删除元素的时间复杂度O(n)
数组的特点是:寻址容易,插入和删除困难;

链表存储区间离散,占用内存比较宽松。
链表是动态分配内存,并不连续。
链表定位元素时间复杂度O(n)
链表插入或删除元素的时间复杂度O(1)
链表的特点是:寻址困难,插入和删除容易。

Node

hashmap中的基础元素:

    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in Linkedhashmap for its Entry subclass.)
     */
    static class Node implements Map.Entry {
        final int hash;
        final K key;
        V value;
        Node next;

        Node(int hash, K key, V value, Node next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry e = (Map.Entry)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

这个就是hashmap中的元素结构,实现了Map.Entry接口,可以看包含了四个属性:hash、key、value、next。

key、value很好理解,分别代表存储的泛型key和对应的泛型value。

next指向的是下一个节点,这里的存储结构是一个单向链表。

hash是指key的hash值,但并不是key.hashcode(),而是通过上文的hash方法得到的值。在jdk1.8中,就是(h = key.hashCode()) ^ (h >>> 16)。

从上面可以知道,hashmap中所有数据都是存在Node[]数据中的,但是怎么用一个数组完成了类似于上图的数组+链表结构呢?下面一一说明。

取get

先看下get的源码:

    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * 

More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise * it returns {@code null}. (There can be at most one such mapping.) * *

A return value of {@code null} does not necessarily * indicate that the map contains no mapping for the key; it's also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases. * * @see #put(Object, Object) */ public V get(Object key) { Node e; return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * Implements Map.get and related methods * * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node getNode(int hash, Object key) { Node[] tab; Node first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }

可以看到,最终实现是在getNode中完成的。

这里详细看下getNode的实现:

先看最外层的if
if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {......}
return null;

这里如果不满足if中的三个条件,就直接返回null。

  • 条件一: (tab = table) != null,判断hashmap中的存储结构 table 是否为空,如果为空,直接中断判断,跳转到最后,返回null
  • 条件二: (n = tab.length) > 0, 判断hashmap中的存储结构 table中是否有元素,如果没有,直接中断判断,跳转到最后,直接返回null
  • 条件三:(first = tab[(n-1) & hash]) != null,注意这里,参照上面的indexFor方法,(n-1) & hash 是取模同样的效果,这里根据hash获取到的index下标,然后判断tab[下标]是否有对应的元素,如果没有,直接跳转到最后,返回null。
看内层的第一个if
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;

这里判断first的第一个元素是不是需要的值,根据hash、key的值是否匹配来决定。

first是根据tab[(n-1) & hash]来取到的,也就是已经确定了数组下标对应的链表。

看内层第二个if
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }

这里是常规操作,链表中第一个元素(头结点)如果不是需要get的值,那么会进入到这段代码。

首先会判断,是否为TreeNode(这是Node的孙子类,上面已有说明),如果是,会进入getTreeNode的逻辑,否则,进入一个do…while循环

do…while循环中,会根据单项链表来依次确认是否有匹配的数据。

存put

看下代码实现:

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with key, or
     *         null if there was no mapping for key.
     *         (A null return can also indicate that the map
     *         previously associated null with key.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node 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 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)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;
    }

可以看到,实际的实现在putVal方法中,老规矩,还是一行一行的看:

第一个if判断:
if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

如果table数组为null,或者数组长度为0,则走到扩容逻辑。

第二个if判断
if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

如果数组对应的下标(通过 (n-1) & hash 来获取,其中n是数组tab的长度)为null,也就是对应位置没有元素,直接塞入即可。

else处理

也就是说,这里是table不为空,且table[i]的位置有元素的情况下,处理的逻辑。分两大步:

第一步,寻找需要将数据插入的位置-即e节点:
            Node 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)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;
                }
            }

e代表需要插入的位置所对应的节点。

else中的第一个if:

            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

在这里,如发现头结点p 其对应的hash、key值完全匹配,那么这个node对象就是要被覆盖的节点。

  • else中的第二个if:
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);

如果头节点已经是treeNode类型,就走到了红黑树的处理逻辑中,这块后续统一梳理。

  • else中的else:

如果需要put的地方既不是头节点,也不是treeNode类型,那么就需要走以下处理:

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

需要注意这三个常量,都是树化相关的

  1. TREEIFY_THRESHOLD=8,根据注释可以知道,如果一个链表的节点数大于等于8,就可能会触发转换为树的操作(为什么是可能呢,这个跟整个table的capacity容量有关,也就是参数MIN_TREEIFY_CAPACITY)。
  2. UNTREEIFY_THRESHOLD 在resize的时候,链表如果重排之后数小于这个值,就会从树状结构回退到单向列表
  3. MIN_TREEIFY_CAPACITY 刚刚说的最小树化容量值。如果一个hashmap的容量值小于这个阈值,频繁扩容的可能性更大,这时候如果树化,会频繁因为resize导致树再转换为链表,得不偿失。所以在treeifyBin中加入了一个capacity的判断,小于MIN_TREEIFY_CAPACITY值,会直接resize。
            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   为什么减一呢?因为马上就要新增一个节点,占据了链表的一个元素计数。这里只需要大于等于7,加上头节点,链表就有至少8个元素。
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

可以看到,这里在for循环中,做了两个if判断处理

  • for中第一个if:

上面已经确定了p节点不是需要被覆盖的node节点,如果p.next是null,那么这里就直接将p.next指向一个新创建节点,新增key、value就在这里轻飘飘的实现了�。这里还做了一个额外的操作,就是判断是否需要树化。

需要注意的是,自己之前的认识有个误区,认为这里e = p.next,然后下面对p.next赋值时,e也应该有值,毕竟是同一个引用。但是这里e的值在 p.next = newNode之后,依然是null。看下面的验证代码:

public class Node {

    String key;

    String value;

    Node next;

    public Node(String key, String value, Node next) {
        this.key = key;
        this.value = value;
        this.next = next;
    }
}

public class NextTest {

    public static void main(String[] args) {

        Node p = new Node("p1","p2",null);
        Node e;
        if((e = p.next) == null){
            p.next = new Node("p3","p4",null);
        }
        System.out.println(e); // null
    }
}

这里在执行第23行,p.next = new Node() 之后,e依然是指向了null,而非新的node节点。

也就是这里,e这个引用通过p.next指向的是null,p.next的引用指向修改后,e的引用并没有发生变化。

  • for中的第二个if:

如果节点e = p.next 不是null,且e的key、hash均与需要put进来的数据匹配,那么e节点会被覆盖,直接跳出。

  • 最后一个 p = e :

相当于p = p.next,继续做循环,在链表中一个节点一个节点的loop下去,如果一直没有需要覆盖的节点,那么就会走到 e = p.next = null的分支,创建一个新节点,并且将上一个节点的next指向这个新节点(p.next = newNode()),然后跳出。这里有点儿绕,但是仔细想了,暂时没发现有别的深意。

这里需要注意的是,之前网上有资料说新增节点会放到头结点,也就是newNode.next = oldNode,但是在jdk1.8代码中反而是 old.next=new,也就是将新数据放在队尾。

第二步,执行替换操作:

如果e不为null,表明e节点这次需要被覆盖,把新value设置进去,所以代码如下:

            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

注意这里的onlyIfAbsent,这个参数默认为false。如果为true,则不修改原数据。

afterNodeAccess的实现为空,看注释是说为LinkedHashMap预留,这里忽略即可。

最后的处理
++modCount;
if (++size > threshold)
    resize();
afterNodeInsertion(evict);
return null;

修改次数 modCount+1,这个修改次数需要注意,如果在foreach的时候对hashmap修改了,会报ConcurrentModificationException,是否修改就是根据操作前后的modCount对比来判断的。

如果size大于扩容阈值,这里会触发扩容动作。

扩容resize

一直在说扩容,终于到了,扩容其实就是对table进行扩展,设定新的capacity、threshold,并迁移oldTab中的数据。先看具体的注释和实现:

    /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node[] resize() {
        Node[] 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[] 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)
                        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 {
                            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;
    }
注意注释:

Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.

啥意思嘞?hashmap每次扩容都是之前的2倍,所以元素要么还在原来的index对应的链表中,要么在移动oldCapacity(原容量)的位置。下面举例说明

比如从16 扩容到 32,即n = 16 —> n = 32

n-1 = 15 ——> 0000 1111

new n-1 = 31 ——> 0001 1111

注意左边新增了一位,这个是关键!

位置变化:

假设一个元素的hash是 0001 0101

扩容之前,oldIndex = (n - 1) & hash = 0101 = 5,也就是index = 5

扩容之后:newIndex = (n - 1) & hash = 10101 = 21,也就是index = 21

21 - 5 = 16 = 原来的capacity

位置不变:

假设一个元素的hash是 0000 0010

扩容之前,(n - 1) & hash = 0010 = 2

扩容之后,(n - 1) & hash = 0010 = 2

扩容前后位置不变

可以看到,一个元素的位置变不变,跟它本身的hash值 在新增的那一位是0 还是 1 有直接关系。

  • 如果是0,那么index计算的时候跟原来保持一致,位置不变。
  • 如果是1,那么index计算的时候,newValue = oldValue + 2的N次方,刚好这个2的N次方,是原来的capacity(因为都是两倍扩容)
  • 所以扩容之后,不需要频繁的移位处理,只需要把对应新增位为1的元素,直接放到newIndex(=oldIndex + oldCapacity )位置即可。这也是hashmap每次扩容变为原有容量2倍的原因之一。

理解了这部分之后,继续去看代码就会简单很多。下面开始代码的学习

代码:
准备工作:
        Node[] oldTab = table;  // 原有的Node数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;  // 原capacity容量
        int oldThr = threshold;   // 原resize阈值
        int newCap, newThr = 0;   // 定义新的capacity、threshold
计算扩容后的容量、阈值
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {  // 极限值,最大容量了,无法再继续扩容
                threshold = Integer.MAX_VALUE;
                return oldTab;    // 直接把threshold设置为最大值,避免再触发扩容,然后返回
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  
                     oldCap >= DEFAULT_INITIAL_CAPACITY)  // 如果原有容量 * 2 小于最大值,且原有容量大于等于16,newCap和newThr都通过左移一位直接设置为原来的两倍
                newThr = oldThr << 1; // double threshold   
        }

        else if (oldThr > 0) // 如果oldThr大于零,oldCap肯定也大于0,这里应该走不到。估计是极端情况下才到
            newCap = oldThr;  

        else {               // 如果刚开始oldCap和oldThr都是0,用默认数据初始化newCap和newThr,分别是16和12
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        // 如果newThr还是0,这里再初始化一次,防呆
        if (newThr == 0) { 
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }

        threshold = newThr; // 把获取到的扩容阈值newThr赋值给实际的threshold,正式生效
        Node[] newTab = (Node[])new Node[newCap];  // 扩容后的Node数组,把容量设置为newCap值
        table = newTab;  // 用实际的newTab 替换掉 原有的table
数据搬迁

需要将数据从oldTab迁移到newTab中,主要是做了注释中说的那段逻辑,即计算新的index值,只有两种结果,不变,或者变为oldIndex+oldCap.

        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {   // 循环处理oldTab中的节点数据
                Node e;  // 处理中的临时节点
                if ((e = oldTab[j]) != null) {  // 获取元素数据,存放在临时节点e中
                    oldTab[j] = null;     // 置空原有节点数据引用,方便后续gc
                    if (e.next == null)      // 如果当前节点是当前链表中最后一个节点,根据e的hash及newTab的      
                        newTab[e.hash & (newCap - 1)] = e;// length计算出新的index,并将e赋值到这个节点中
                    
                    else if (e instanceof TreeNode)      // 如果是红黑树,转到split方法中处理
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    
                    // 以上两种情况都不是,说明是小于等于8个节点的链表数据处理.
                    // 下面就是需要处理具体的数据搬迁了,主要关注index值的计算
                    else { // preserve order       
                        Node loHead = null, loTail = null;  // index不变的数据链表,low
                        Node hiHead = null, hiTail = null;  // newIndex=oldIndex+oldCap的数据,high
                        Node next;
                        // 循环处理,这里处理的是index=j的一个链表中的所有数据,这里是尾部插入
                        do {
                            next = e.next;  // e = oldTab[j]  通过next做循环,第一次e是头结点
                            
                            // 假设 oldCap = 16,也就是 0001 0000
                            // e.hash = 0001 1010 0000
                            // 那么 e.hash & olcCap = 0000 0000 0000,也就是等于0
                            // 注意这里做&操作的,不是oldCap-1,而是oldCap,都是2的N次方,也就是只有一位是1
                            
                            // 这里是等于0的,也就是index不变的,newIndex = oldIndex
                            if ((e.hash & oldCap) == 0) {
                                
                                if (loTail == null)  // 由于采用尾插方式,所以刚开始loTail=null
                                    loHead = e;  // 设定loHead=第一个元素,作为newTab[j]的头结点
                                else
                                    loTail.next = e;  // 如果loTail != null,不是链表第一个节点,尾插
                                
                                loTail = e;  // 设定loTail为e,循环下一个。
                                // 注意这时候,loHead 和 loTail都指向同一个节点,也就是头节点,所以loHead.next = 下一个处理节点,真神奇的写法,赞!
                            }
                            
                            
                            // oldCap = 16 = 0001 0000
                            // e.hash = 0001 1001 0010 
                            // e.hash & oldCap = 0000 0001 0000  != 0
                            // 所以这里是index变化的,newIndex = oldIndex + oldCap
                            else {
                                if (hiTail == null)  // 第一个迁移过来的元素处理时,hiTail=null
                                    hiHead = e;      // 设置 hiHead = 第一个元素,作为newTab[j+oldCap]的头节点
                                else
                                    hiTail.next = e;  // 如果hiTail != null,不是链表第一个节点,尾插
                                
                                hiTail = e;
                                // 同理,这时候hiHead 和 hiTail都指向同一个节点,也就是头节点,所以hiHead.next = 下一个迁移过来的节点,再次赞一个!
                            }
                            
                        } while ((e = next) != null); // 更新e的值为next,也就是指向下一个节点,也就是做遍历条件更新
                        
                        // 将loHead作为newTab的第j个节点,loHead.next = 后面的元素链表
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 将hiHead作为newTab的第j+oldCap个节点,hiHead.next = 后面的元素链表
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab; // 循环结束后,处理完成,返回newTab

其实具体实现的点都在代码里面说明了,这里再着重总结以下:

  1. newIndex = oldIndex 或者 oldIndex + oldCap
  2. 迁移过来的元素采用尾插方式,也就是从链表尾部插入。
  3. loHead 和 loTail 处理的是index不变的数据,也就是newIndex = oldIndex的数据。loHead指头结点,只用两次,一次是刚开始处理时的赋值,一次是放到newTab时的引用。
  4. hiHead 和 hiTail处理的是index变化的数据,也就是newIndex = oldIndex + oldCap的数据。同理,hiHead也是头结点。

compute & merge & foreach

新增的部分实现,先不啃了,暂时没那么常用,大概扫一扫。

红黑树的相关的地方,参考下一篇文章。

你可能感兴趣的:(HashMap小探(二)—存、取、扩容)