HashMap 源码解析(基于 JDK 1.8)

源码环境: JDK 1.8

本文不介绍红黑树节点的处理过程。

在 1.8 中,HashMap 是数组+链表+红黑树。

1 常用变量及节点类

如图所示,在下面的 HashMap 中,桶数组 table 有着 64 个 Node,大小 size 是 3+1+9 = 13。这个 HashMap 中只有三个桶非空:table[0] 是一个长度为 3 的链表,table[1] 是一个长度为 1 的链表,table[3] 是一个长度为 9 的红黑树。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;// 默认初始容量 16
    static final int MAXIMUM_CAPACITY = 1 << 30;// 最大容量2^30
    static final float DEFAULT_LOAD_FACTOR = 0.75f;// 默认装载因子
    static final int TREEIFY_THRESHOLD = 8;// 桶数组某位置链表节点超过这个数,则从链表转变为红黑树
    static final int UNTREEIFY_THRESHOLD = 6;// 调用 split 时,桶数组某位置链表节点不大于这个数,则从红黑树转链表。
    static final int MIN_TREEIFY_CAPACITY = 64;// 桶数组容量小于这个数,则不转化为红黑树,而是扩容 resize
    
    transient Node<K,V>[] table;// 桶数组
    transient Set<Map.Entry<K,V>> entrySet;// 并不存储数据,可以理解为视图
    transient int size;// 键值对的总数
    transient int modCount;// 用于快速失败
    int threshold;// 阈值,table 初始化后等于 capacity*loadFactor
    final float loadFactor;// 装载因子
    final int capacity() {
        return (table != null) ? table.length :// table 不空则是 table 的长度
            (threshold > 0) ? threshold :// 否则是 threshold
            DEFAULT_INITIAL_CAPACITY;// 否则是默认容量
    }

节点有两种,即 Node 和 TreeNode,下面的类图展示了两者的关系,有两个 Entry,上面的 Entry 是 Map.Entry,下面的是 LinkedHashMap.Entry 。简单地说,TreeNode 也是 Node 的子类,所以 TreeNode 也有 Node 的属性。需要关注的是 Node.next,在后面使用迭代器进行遍历时,并不区分是红黑树还是链表,统一用 next 获得下一个。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}
        
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;
}
            
        
        
static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

2 构造器及 tableSizeFor

一共四种构造器,无参构造器(以下称为构造器1)会初始化装载因子,含有 initialCapacity 的构造器(以下称为构造器2)调用另一个构造器,含有 initialCapacity 和 loadFactor 的构造器(以下称为构造器3)初始化了 loadFactor,以及 threshold,在 put 时才真正初始化桶数组。含有 Map 的构造器(以下称为构造器4)则初始化 loadFactor 并添加进所有元素。

public HashMap() { 
    // CAPACITY默认16,装载因子默认0.75,所以默认threshold是12,这些初始化操作在 resize 执行
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

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

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);// 获取一个2的幂的结果,这里表示的其实是容量,未乘loadFactor。
    }

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

接下来介绍 tableSizeFor(initialCapacity),putMapEntries 放在后面介绍。

static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

下面假定 cap 为非负数,取值范围是[0, \(2^{31}\)-1]。根据代码,可以得到下面的分段函数,y 为返回的结果。

\[ y= \begin{cases} 1, & cap = 0,\\ 2^{30}, & cap > 2^{30},\\ 大于等于cap的最小的2的幂次, & cap为其他 \end{cases} \]

解释下 cap 为其他的具体处理。先计算 int n = cap -1,cap 分为两种情况,是 2 的幂,或者不是 2 的幂。如果是 2的幂,则 n 的最高位的 1 是 cap 最高位的 1 向右移位;否则,两者最高位的 1 是在同一个位置。

下面进行五次移位以及或操作,最终得到的 n 是:从刚开始算出的 n 的最高位的 1 的位置,后面的每一位都变成1。然后 n+1 返回的正好是 2 的幂。

如图,分别展示了 cap 为 256 和 cap 为 257 的情况。图中每 8 位算一组,填充为橙色的表示 1,否则为 0。第三行的 newN 是算出的结果。

3 put相关方法

3.1 put/putIfAbsent/putAll

put 和 putIfAbsent 的区别在于是否第四个参数,true 表示只有 key 不存在时插入,或者 key 对应的是 null 时覆盖原来的 value;false 表示总是可以插入或者覆盖原来的 key 对应的 value。

putAll 调用了 putMapEntries,第二个参数为 true,表示是放入而不是新建。具体是调用了 afterNodeInsertion,在 HashMap 中无意义。

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);// 允许覆盖原来的 key 的 value
    }

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

public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);
    }

3.2 putMapEntries

该方法被 putAll 和构造器4使用,在构造器中 evict 为 false,在 putAll 中为 true,false 表示是初始构建该 map。

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                //首先要找到一个capacity,capacity = s/loadFactor。
                //为了避免出现小数,取整变成向下取整,则+1.0f
                float ft = ((float)s / loadFactor) + 1.0F;
                // 不允许 t 超过 MAXIMUM_CAPACITY
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
        // threshold太小则重置threshold,这里threshold代表capacity,在resize()里*loadFactor
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)// table不空但Map的大小大于threshold
                resize();//扩容
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);// 逐个插入
            }
        }
    }

3.3 hash

计算 key 的 hash值:如果 key 为 null,则返回0。否则记 h = key.hashCode(),返回 h ^ (h>>>16)。

这个 >>> 表示无符号右移,即将 h 向右移16位,低 16 位都移出去了,高 16 位补 0。

^ 表示异或,对应位置相同(都是1或0)为0,不同为1。

这里进行右移和异或的作用是:考虑到哈希表长度一般都不大,如果只用 hashCode 来寻址,则取模的时候高位的值没有被用上,冲突的概率更大。为了减少冲突,进行上述操作。

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

3.4 putVal

处理过程:

  1. 判断数组table[]是否为空或为null,如果是,执行resize()进行初始化;

  2. 进行下列操作

    2.1 由 (n - 1) & hash得到插入的桶数组的位置 ,如果对应的桶为空,则新建节点。

    2.2 否则,记 e 为:满足 e.key 和 插入的 key 相同,且在未插入前就存在的 Node,用于处理是否覆盖原来 key 对应的 value 的情况。**

    2.2.1 如果桶中的元素对应的 p.key 和 出去的 key 一致,则设置 e = p。**

    2.2.2 否则,判断桶中节点是否为红黑树的节点,是则执行红黑树的插入操作。**

    2.2.3 否则,当前桶存储的是链表,只需要不断向后查找,如果找到相同的 key,则退出;否则,在末尾插入新节点。**

    2.2.3.1 插入后,判断当前桶的链表元素个数是否超过 TREEIFY_THRESHOLD(原来链表中有TREEIFY_THRESHOLD 个,再插入一个,才满足条件),是的话则尝试将链表转换为红黑树(不一定转换,还要满足 MIN_TREEIFY_CAPACITY 的条件)**

    2.2.4 如果 e != null,说明相同的 key 出现过了,如果对应的值是 null 或者onlyIfAbsent 为 false,可以覆盖;否则,不能覆盖。返回旧值。

  3. ++size,如果元素个数大于 threshold,执行扩容 resize。**

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果 table 为空或者是长度为0,要初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    //如果 table 中对应位置 (n - 1) & hash 的桶为空,则新建一个节点
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // e 为:满足 e.key 和 插入的 key 相同,且在未插入前就存在的 Node。
            //用于处理是否覆盖原来 key 对应的 //value 的情况。
            Node<K,V> e; K k;
            // 该桶中第一个 Node 的 key 和上面的 key 相等,不需要继续查找
            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) {
                    //遍历到末尾没找到相同的 key,则插入到最后(尾插)。
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //插入之前的桶中元素个数是 TREEIFY_THRESHOLD,则尝试转换为红黑树。
                        //不一定转换,还要满足 MIN_TREEIFY_CAPACITY 的条件。
                        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;
                }
            }
            // 如果对应的 key 已存在相应的值
            if (e != null) { 
                V oldValue = e.value;
                // onlyIfAbsent 为 false 或者旧 value 是 null,则可以替换。
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                //返回旧值
                return oldValue;
            }
        }
        ++modCount;
    // 先 ++size,再比较 threshold,所以是判断插入后的整个 map 中 Node 的个数是否大于 threshold
        if (++size > threshold)
            resize(); //扩容
        afterNodeInsertion(evict);
        return null;
    }

有几点需要解释:

  1. 由于 n = tab.length 总是 2 的幂,在正数情况下,总有 hash % n == hash & (n-1),用位运算 & 是为了加快计算。

    以 100 % 16 为例,100 省略前面的 0 可以写成二进制形式 110 0100,16 写成二进制形式 10000
    100 = (1 * 2^6 + 1 * 2^5 + 0 * 2^4)+ 4
    100 % 16 = ((...) + 4 )% 2^4 = 4 % 2^4 = 4
    也就是说,100 的二进制只保留后四位,而前面的一定是 16 的倍数,取模会去掉。
    保留后四位也就是 & 1111,所以 100 % 16 == 100 & (16-1),其他同理可得。 
    
  2. 比较元素 p.key 和 key :这里用的是 p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))

    首先比较 hash 值,相等再比较是否 == 或者 key不为 null 时比较 equals。

    2.1 如果 hash 值不同则不可能是相同的 key,不满足条件。先比较 hash 可以较快的排除很多不可能的 key。

    2.2 如果两个 key 是同一个,满足条件。 == 通常比 equals 要快。

    2.3 否则,key != null 时,如果 key.equals(k),也满足条件。

    如果 key 是 null,要相等 k 也要是 null,这种情况在 2.2 已经处理了。这三种判断是从快到慢的,这样设计是为了加快判断的速度。

  3. 在上述处理步骤 3.3.1 中,插入后的元素个数是 binCount+2,要求插入后满足if (binCount + 2 > TREEIFY_THRESHOLD),或者是if (binCount + 1 >= TREEIFY_THRESHOLD),也就是if (binCount >= TREEIFY_THRESHOLD - 1)

3.5 resize

resize 用于三种情况:初始化时,插入后元素个数超过阈值时,treeifyBin 时桶数组的元素个数 < 64时。

处理过程:

  1. 首先根据旧容量 oldCap 和旧阈值 oldThr 的值进行扩容或者初始化

    1.1 如果 oldCap > 0

    1.1.1 如果 oldCap >= MAXIMUM_CAPACITY,不能扩容了,只是增大 threshold,然后返回

    1.1.2 否则,如果 (newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY,将旧阈值加倍获得新阈值

    1.2 否则,如果 oldThr > 0,这种情况是初始化了 threshold,但未初始化 table。这时的 threshold 是 tableSizeFor,是2的幂,将这个值直接赋给newCap。

    1.3 否则,说明应该是空参构造器,没有初始化 threshold 和 table,在这里计算 newCap 和 newThr。

  2. 判断 newThr 是否为 0,对应于 1.1 中 1.1.1 和 1.1.2 都不满足的情况和 1.2 的情况。这时候需要计算出一个新的 newThr,由 ft = (float)newCap * loadFactor计算出一个 ft。如果 newCap 和 ft 都小于 MAXIMUM_CAPACITY,则 ft 取整,否则是 Integer.MAX_VALUE。

  3. 构建新桶 newTab,将其置为 table,如果原桶数组 oldTab 不空,则遍历里面所有的桶。在处理某个桶的时候,如果这个桶不空,则进行处理,将这个桶中的元素都放到新桶数组的对应位置,记这个桶为 oldTab[j]。

    3.1 如果这个桶只有一个元素,则将元素 e 放在 newTab 的 e.hash & (newCap - 1)] 位置。

    3.2 如果这个桶对应一个红黑树,按照红黑树的方法 split

    3.3 如果这个桶对应一个链表,遍历链表中每一个元素,并根据 e.hash & oldCap 是否为0将这个元素插入 lo 或者 hi 链表后面。遍历完所有元素之后,lo 不空则放入 newTab[j],hi 不空则放入newTab[j+oldCap]。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;// 旧容量
        int oldThr = threshold;// 旧阈值
        int newCap, newThr = 0;// 新容量和新阈值
    // table非空的情况
        if (oldCap > 0) {
            // oldCap太大,不能再扩容了,增大 threshold 并返回。
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // newCap也小于 MAXIMUM_CAPACITY 且 oldCap 大于 16
            // 将阈值也扩容
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
    // 含参数构造器(2,3,4)初始化了threshold,但没有初始化 table
    // 此时 threshold 没乘装载因子,所以赋给 newCap 
        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);
        }
    // 对应上面的两种的情况,会重新计算newThr
        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"})
    // 新建 hash 桶数组,并赋给 table。
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
    // 原来的桶数组不空,要把原来桶数组的 Node 重新计算位置后放入新的桶数组对应的位置。
        if (oldTab != null) {
            // 遍历 oldCap 中每个桶
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                // 如果桶不空
                if ((e = oldTab[j]) != null) {
                    // 将旧桶置空
                    oldTab[j] = null;
                    // 如果只有一个元素,将该元素直接放入newTab的位置
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e; //新位置
                    // 如果是红黑树的情况
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // 否则,是链表的情况,具体是将该链表拆分成两个链表lo和hi,放入newTab对应位置
                    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;
                            // 构建低位置的链表lo,使用尾插法
                            if ((e.hash & oldCap) == 0) { 
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // // 构建高位置的链表ho,使用尾插法
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //lo不空则放在newTab和oldTab同样的位置
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //hi不空则在newTab的新位置(即newTab[j+oldCap],其中j为oldCap中旧位置)
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

这里解释一下链表拆分即(3.3) 的原理:两个元素 e1 和 e2,oldCap = \(2^8\),newCap = \(2^9\),观察变化情况。

主要分析两点,

  1. e.hash & (newCap -1) 和 e.hash & (oldCap -1)的关系:

    前者是 Node e在新表的位置,后者是 e 在旧表的位置。

    有两种情况:

    1.1 位置不变,即在链表 lo

    e.hash & (newCap -1)= e.hash & (oldCap -1)

    1.2 位置变成原来的位置加上 oldCap,即在链表 hi

    e.hash & (newCap -1)= (e.hash & (oldCap -1)) + oldCap

    具体是哪一种情况在于多出来的最高位 1 异或的结果是0还是1。

  2. 使用 e.hash & oldCap == 0 的意义:

    如果 e.hash & oldCap == 0,则对应 1.1的情况;否则,对应 1.2 的情况。

e1.hash    = xxxxxxxx xxxxxxxx xxxxxxx0 10000001  (1)

e2.hash    = xxxxxxxx xxxxxxxx xxxxxxx1 10000001  (2)

oldCap - 1 = 00000000 00000000 00000000 11111111  (3)

newCap - 1 = 00000000 00000000 00000001 11111111  (4)

oldCap     = 00000000 00000000 00000001 00000000  (5)

(1) & (3)  = 00000000 00000000 00000000 10000001  (6)

(2) & (3)  = 00000000 00000000 00000000 10000001  (7)

(1) & (4)  = 00000000 00000000 00000000 10000001  (8)

(2) & (4)  = 00000000 00000000 00000001 10000001  (9)

(1) & (5)  = 00000000 00000000 00000000 00000000  (10)

(2) & (5)  = 00000000 00000000 00000001 00000000  (11)

由于 (6) == (7),则 e1 和 e2 在旧桶数组 oldTab 的同一个位置,即 oldTab[129]。
由于 (8) != (9),则 两个元素在新桶数组 newTab 中不在同一个位置,e1 在 newTab[129],
e2 在 newTab[129+oldCap]。
注意到 (10) != (11),newCap - 1 的最高位正好对应着 oldCap,想让这一位和 e.hash 做 &,也就是直接
e.hash & oldCap。如果这一位是0,即 e1 的情况,在低的位置 newTab[129];如果这一位是1,即 e2 的情况,
在高的位置 newTab[129+oldCap]


在扩容后,新位置是 e.hash & (newCap -1),注意观察,由于 oldCap 和 newCap 都是2的幂,newCap - 1 比
起 oldCap - 1 多出一位,在进行 & 时对于 e 需要多考虑一位,区别只在于这一位,有两种情况:如果新增加的这一
位的位置在 e.hash 的对应位置是0,则 e.hash & (newCap -1) == e.hash & (oldCap -1);如果新增加的这
一位是对应的是1,则 e.hash & (newCap -1) == (e.hash & (oldCap -1) + oldCap)。

若 e.hash & oldCap ==0,说明 e.hash 在 (newCap -1) 的最高位的位置是0,所以 e.hash & (newCap-1) 
在这个最高位会得到 0,也就是保留在原来的位置 newTab[j];否则,说明 e.hash 在 (newCap -1) 的最高位的位
置是1, e.hash & (newCap-1) 在这个最高位会得到 0,也就是移动到新位置 newTab[j+oldCap]

4 get 相关

4.1 get/getOrDefault

两者都调用了 getNode。

对于 get 如果 getNode 是 null 则返回 null;对于 getOrDefault 如果 getNode 是 null 则返回 defaultValue。

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

public V getOrDefault(Object key, V defaultValue) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
    }

4.2 getNode

处理过程:

  1. 如果 table 已经初始化,table 长度大于0,table[(n-1)&hash] 中第一项也不为空,进行下面的操作:

    1.1 先判断第一个元素是不是要找的;

    1.2 如果第一个元素的后一个不为空

    1.2.1 如果是红黑树,按红黑树的方式查找

    1.2.2 如果是链表,则遍历查找

  2. 不满足条件,直接返回 null。

这里判断两个 key 是否相等的逻辑和 putVal 中的一样,不再解释。

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 先判断 1. 是否成立
        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;
            // 如果第一个元素的下一个非 null
            if ((e = first.next) != null) {
                // 如果是红黑树节点
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)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;
    }

5 remove 相关

5.1 remove

remove 有两个方法:对于只给定 key 的方法,删除时只比较 key 就可以;对于 给定 key 和 value 的方法,要求 map 中的 key 正好对应相等的 value 才可以删除。

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }

5.2 removeNode

处理过程:

  1. 如果 table 已经初始化,table 长度大于0,table[(n-1)&hash] 中第一项也不为空,进行下面的操作:

    1.1 先判断第一个元素的 key 是否相等;

    1.2 否则,如果第一个元素的后一个不为空

    1.2.1 如果是红黑树,按红黑树的方式查找相等的key

    1.2.2 如果是链表,则遍历查找相等的key

    1.3 根据 matchValue 确定是否需要相同的 value才删除,如果删除

    1.3.1 如果是红黑树节点,按照红黑树的方法删除

    1.3.2 如果是桶数组在这个位置的桶的第一个元素,则将这个元素的下一个放在桶中。

    1.3.3 如果是桶数组中非第一个元素,直接删掉就行。

  2. 不满足条件,直接返回 null。

//第四个参数 matchValue 表示是否要求 value 相等才能删除,如果是 true,则 value 必须相等。
//第五个参数 movable 表示删除后是否移动节点,如果为 false,则不移动,与红黑树删除节点有关。
final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 先判断 1 是否成立
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            //使用 node 记录可能出现的key相等的元素,初始为null
            Node<K,V> node = null, e; K k; V v;
            // 先检查第一个节点
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 将 node 修改为 p
                node = p;
            // 如果后面还有节点
            else if ((e = p.next) != null) {
                //如果是红黑树,按照红黑树的方法查找
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                //否则,则是链表,遍历查找
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        // 如果在某一步 break,则 p 是 node 的前一个元素
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                // 如果是红黑树
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                // 如果是第一个元素,则将第一个元素换成下一个
                else if (node == p)
                    tab[index] = node.next;
                // 其他情况直接删除
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

解释一下 1.3 中的判断if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v))))

  1. 首先要满足能找到 key 相同的节点,也就是 node != null
  2. 如果 matchValue 为 false,则不需要比较 value,也就是 ! matchValue
  3. 如果 matchValue 为 true,需要比较 value,也就是(v = node.value) == value ||(value != null && value.equals(v)),和 putVal 中比较 key 是同样的逻辑,不再解释。
  4. 上面两个满足或关系,用 ||来连接。

6 replace

两参数的 replace 首先找到对应 Node e,直接 e.value = value。

三参数的 replace 也是找到对应的节点 Node e,但要求 e.value 和 oldValue 相等才可以替换。

replaceAll 是将每个节点 e 的 value 都替换为 function.apply(e.key, e.value),注意这里没有区分红黑树和链表,统一用 next 查找同一个桶中的下一个。

public V replace(K key, V value) {
        Node<K,V> e;
    //根据 key 找到对应的节点 e
        if ((e = getNode(hash(key), key)) != null) {
            V oldValue = e.value;
            // 替换
            e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
        return null;
    }

public boolean replace(K key, V oldValue, V newValue) {
        Node<K,V> e; V v;
    // 根据 key 找到对应的节点 e,&&后面要求 e.value 和 oldValue 相等。
        if ((e = getNode(hash(key), key)) != null &&
                ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
            e.value = newValue;
            afterNodeAccess(e);
            return true;
        }
        return false;
    }

public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
        Node<K,V>[] tab;
        if (function == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                // 对于一个桶中的元素,统一用 next 查找下一个
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    e.value = function.apply(e.key, e.value);
                }
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }

7 钩子函数

在 HashMap 中无意义,在 LinkedHashMap 中使用,可以关注我,看我后续关于 LinkedHashMap的文章。

// Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }

8 键值对集合

KeySet 和 EntrySet 继承了 AbstractSet,Values 继承了 AbstractCollection。

三个类的 forEach 类似,都是遍历桶数组中每一个桶,对于每个桶,不管是红黑树还是链表,都是用 next 获取桶中下一个元素。

final class KeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<K> iterator()     { return new KeyIterator(); }
        public final boolean contains(Object o) { return containsKey(o); }//containsKey
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        public final Spliterator<K> spliterator() {
            return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super K> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    // 使用 next 获取下一个元素
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

final class Values extends AbstractCollection<V> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<V> iterator()     { return new ValueIterator(); }
        public final boolean contains(Object o) { return containsValue(o); }
        public final Spliterator<V> spliterator() {
            return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super V> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    // 使用 next 获取下一个元素
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e.value);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

 final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Node<K,V> candidate = getNode(hash(key), key);// 主要查找步骤
            return candidate != null && candidate.equals(e);
        }
        public final boolean remove(Object o) {
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                Object key = e.getKey();
                Object value = e.getValue();
                return removeNode(hash(key), key, value, true, true) != null;//主要删除步骤
            }
            return false;
        }
        public final Spliterator<Map.Entry<K,V>> spliterator() {
            return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
                Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    // 使用 next 获取下一个元素
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

KeySet 和 EntrySet 的 contains 用的都是 getNode,remove 都是 removeNode。

Values 的 contains 内部是遍历查找。

public boolean containsKey(Object key) {  // 用于 KeySet
        return getNode(hash(key), key) != null;
    }
    
public boolean containsValue(Object value) {// 用于Values
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

// 下面为 Node 中的 equals 方法,用于 EntrySet 的contains。
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;
        }

9 迭代器

三个迭代器 KeyIterator、ValueIterator 和 EntryIterator 都是继承了 HashIterator,重点在于 HashNode 的nextNode 方法。

其实 nextNode 和上面的 forEach 逻辑基本一致,都是遍历桶数组中的每一个桶,并对当前的桶不断使用 next 获取下一个,直到当前桶没有元素为止。

在后面重写了 nextNode 方法。

abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry  用于删除
        int expectedModCount;  // for fast-fail
        int index;             // current slot   当前 Node 在桶数组的哪一个桶的位置

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry 遍历,找到第一个非空元素
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

    final class KeyIterator extends HashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }

    final class ValueIterator extends HashIterator
        implements Iterator<V> {
        public final V next() { return nextNode().value; }
    }

    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

下面重写了 nextNode 方法,意思不变,方便理解,称为 nextNode2,修改最后一个 if,将其注释掉,新加的代码用一个{}放在一个代码块中。

解释一下:

在原来方法的第二行,将 next 赋给 e,在修改的部分又将 e 赋给current,这两句合起来就是 current = next ; 然后 next 取下一个。如果 next 为 null,则获取当前的表 table,未越界,则获取接下来的 t[index],直到找到一个位置满足 t[index] != null,这个位置对应的桶的第一个元素就是 next。

 final Node<K,V> nextNode2() {
            Node<K,V>[] t;
            Node<K,V> e = next; // 首先将 next 赋给 e。
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            //if ((next = (current = e).next) == null && (t = table) != null) {
            //    do {} while (index < t.length && (next = t[index++]) == null);
            //}
     { //新添加的代码块
            current = e;
            next = current.next;// next 取下一个
            if  (next == null){// 如果为 null
                t = table;
                if (t != null){
                    while (index < t.length){
                        next = t[index];// 尝试获取 t[index] 处的元素
                        index++;  
                        if (next != null){// 如果是 null,继续循环查找,否则可以退出循环并返回。
                            break;
                        }
                    }   
                }
            }
     } //新添加的代码块
 
     
     
            return e;
        }

下面是我的公众号,Java与大数据进阶,分享 Java 与大数据笔面试干货,欢迎关注

posted @ 2021-02-22 11:18  Java与大数据进阶  阅读(37)  评论(1编辑  收藏  举报