HashMap系列:put元素(不看完将后悔一生!)

(一)HashMap系列:负载因子0.75
(二)HashMap系列:树化阀值8,退化阀值6
(三)HashMap系列:2次方扩容
(四)HashMap系列:put元素(不看完将后悔一生!)

红黑树系列:
一、《算法—深入浅出》N叉树的介绍
二、《算法—深入浅出》红黑树的旋转
三、《算法—深入浅出》红黑树的插入
四、《算法—深入浅出》红黑树的删除

一、前言

建议在学习本节内容前,先把以上目录中的内容先了解一遍再来学习本篇,更加容易理解 HashMap 的思想。

二、HashMap延迟初始化

我们通常使用 HashMap 方式:

Map map = new HashMap();

HashMap默认构造函数:

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

我们看到,构造方法中,只初始化了负载因子:0.75;并没有初始化数组(即分配内存)。

HashMap只有在存放第一个键值对时才初始化(put / putIfAbsent 都会调用 putVal):

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
        
    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; // table为null时,调用resize进行首次初始化
        ......
    }
    
}

HashMap.resize:

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
    
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    final Node[] resize() {
        Node[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            ......
        }
        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);
        }
        ......
        
        @SuppressWarnings({"rawtypes","unchecked"})
        Node[] newTab = (Node[])new Node[newCap]; // 创建新的对象数组
        
        .....
        return newTab;
    }
}

三、添加对象

3.1、HashMap.put / putIfAbsent

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
    
    // 直接存放,若存在则直接替换覆盖
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    // 如果存在则直接返回已存在的值,不存在则存入
    @Override
    public V putIfAbsent(K key, V value) {
        return putVal(hash(key), key, value, true, true);
    }
}

3.2、HashMap.putVal

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
        
    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;
            
         // 计算数组下标 => i,获取下标对应的对象 => p
        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;
    }
}

代码看起来较多,较复杂,容我将其拆解,一一分析:

  • 判断是否为首次初始化(上面延迟初始化一节已经分析,这里不再分析)
public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
        
    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;
        ......
    }
}
  • hash后的下标对应的数组坑位为 null,则直接放入即可
public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
        
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        ......
         // 计算数组下标 => i,获取下标对应的对象 => p
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        ......
    }
}

以下具体看注释:

  • 情况A:hash后,数组对应的下标已经有元素,则判断是否为同一元素(同hash,key同内存地址,key同值);
  • 情况B:红黑树查找(找到直接返回) / 插入节点(未找到);
  • 情况C:单链表查找(找到还要判断情况A:是否为同一元素);未找到先插入到链表,再判断是否需要转为红黑树;
public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
        
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        ......
         // 计算数组下标 => i,获取下标对应的对象 => p
        if ((p = tab[i = (n - 1) & hash]) == null)
            ......
        else {
            Node e; K k;
            // 情况A
            // 相同的 hash、key,且值也相等
            if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                
            // 情况B(红黑树)
            // 数组下标的节点是红黑树(而且还是树根)
            // putTreeVal:
            // 1. 先查找,若找到则返回该节点
            // 2. 没找到(一定到了叶子 nil),插入节点并返回 null
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 情况C(单链表)
                // 从链表头依次查找,若到链尾,则插入节点
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        
                        // 链表插入节点后,需要判断是否达到树化的阀值(8 - 1 = 7)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break; // break时,e = p.next = null
                    }
                    
                    // 若找到,需要判断(情况A)
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break; // break时,e = p.next != null
                    p = e; // p = p.next,继续循环
                }
            }
            ......
        }
        ......
    }
}
  • 元素已经存在的处理情况
public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
        
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        ......
        else {
            ......
            // 到这里,表明元素要么已经存在 e 为旧值
            // 要么 e 为null,表明插入到链表 或者 红黑树中
            
            if (e != null) { // 待放入的元素已经存在
                V oldValue = e.value; // 先获取旧值
                if (!onlyIfAbsent || oldValue == null) // 若允许替换 或者 旧值为 null,则用新值替换(覆盖)
                    e.value = value;
                afterNodeAccess(e); // LinkedHashMap 才会用到,HashMap 中该函数为空函数
                return oldValue; // 若不许替换则返回旧值,否则返回新值
            }
        }
        ......
    }
}
  • 新增元素后还要判断是否需要扩容()
public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
    
    // 返回为 null 表明是新增元素;
    // 返回有值,则表明之前就存在;
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        ......
        
         // 到了这里,一定是新增了一个元素(无论是桶、单链表、还是红黑树),只要新增了元素,size就加1
        // 判断数组新增了元素后,是否达到扩容阀值:负载因子 * Capacity
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict); // LinkedHashMap 才会用到,HashMap 中该函数为空函数
        return null;
    }
}

四、HashMap 极端测试(一定要看!)

看到最后一点(新增元素也要判断是否扩容,而不是 hash 数组占用率达到扩容阀值才考虑扩容),我以为我的源码有问题,因此,我写了一个测试用例来验证我的猜想(再次申明:我的源码是 JDK1.8)

先说说我的猜想:

  • 前提:源码是无论新增在哪里(桶、链表、还是红黑树),都对 size + 1;
  • 构造:我创造一个特殊类,它的 hash 值永远固定,但它的 equals 确不想等;这样,就能造成 hash 冲突;
  • 结论:我先说结论吧,确实 resize 了,不过,出现了意外的惊喜;

构造特殊类:

public class SpecialClass {
    @Override
    public int hashCode() {
        return 1; // 强制 hash 冲突
    }

    @Override
    public boolean equals(Object obj) {
        return false; // 强制两对象不相同
    }
}

测试代码:

import java.lang.reflect.Method;
import java.util.HashMap;

public class Main {

    public static void main(String[] args) {
        HashMap map = new HashMap<>();
        Method capacity = null;
        try {
            // 反射:每次新增元素后,查看是否 size+1 导致 resize
            capacity = map.getClass().getDeclaredMethod("capacity");
            capacity.setAccessible(true);

            for (int i = 0; i < 13; i++) {
                SpecialClass specialClass = new SpecialClass();
                map.put(specialClass, specialClass);

                System.out.println("[" + i + "] map.size = " + map.size() + ", capacity = " + capacity.invoke(map));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

实际输出结果:

 [0] map.size = 1, capacity = 16
 [1] map.size = 2, capacity = 16
 [2] map.size = 3, capacity = 16
 [3] map.size = 4, capacity = 16
 [4] map.size = 5, capacity = 16
 [5] map.size = 6, capacity = 16
 [6] map.size = 7, capacity = 16
 [7] map.size = 8, capacity = 16
 [8] map.size = 9, capacity = 32
 [9] map.size = 10, capacity = 64
[10] map.size = 11, capacity = 64
[11] map.size = 12, capacity = 64
[12] map.size = 13, capacity = 64

每次新增元素,size 确实是加 1,调试 HashMap 源码:

  • [0] 放在桶中;
  • [1] ~ [7] 放在单链表中;
  • [8] 先插入链表,按照源码,应该转成红黑树,但是,我们在讲《HashMap系列:树化阀值8,退化阀值6》中说了,树化开启是需要 capacity = 64,然后,你发现打印出来的 log 竟然扩容了;
  • [9] 又再次导致扩容;
  • [10 ~ 12] 之后就不再扩容了;

想必大家已经想迫不及待的想听我的分析了,OK,这就GO!

上面的分析,问题肯定是出在单链表转红黑树这里,那么我们就直接看『树化』的代码:

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
        
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        ......
         // 计算数组下标 => i,获取下标对应的对象 => p
        if ((p = tab[i = (n - 1) & hash]) == null)
            ......
        else {
            // 情况A
            // 情况B(红黑树)
            ......

            else {
                // 情况C(单链表)
                // 从链表头依次查找,若到链尾,则插入节点
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 链表插入节点后,需要判断是否达到树化的阀值(8 - 1 = 7)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        
                            // 我们期望的答案就在这个树化方法里
                            treeifyBin(tab, hash);
                        break; // break时,e = p.next = null
                    }
                    ......
                }
            }
            ......
        }
        ......
    }
}

单链表转红黑树(treeifyBin)

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
        
    final void treeifyBin(Node[] tab, int hash) {
        int n, index; Node e;
        // 重点在这里:如果当前桶的大小,即 capacity < 64,则扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
            
         // 树化过程
        .....
    }
}

然后,你就会发现一个奇怪的事情:

  • 初始Capacity = 16:不断添加元素,直到单链表(达到阀值 8 - 1 = 7),申请转红黑树,结果红黑树直接让 HashMap 扩容 Capacity = 32,但!但是!扩容后,链表中的元素仍旧停留在统一的链表中,并没有高低位链表区分,因此,此时单链表长度是(8 - 1 = 7,不含桶中的第一个元素);
  • 当前Capacity = 32:再添加一个冲突的元素,此时还是先链表逻辑,追加到表尾,然后申请转红黑树,结果不满足树化条件,因此,扩容 Capacity = 64,同理,所有元素仍在链表中,此时,链表长度是(9 - 1 = 8,不含桶中的第一个元素);
  • 当前Capacity = 64:再添加一个冲突的元素,此时还是先链表逻辑,追加到表尾,然后申请转红黑树,此时满足树化条件,因此,链表转红黑树;
  • 后面相同冲突的元素,都直接添加到红黑树中;

也许大家没注意,也许大家知道没说,我嘛,发现了,一定要分享给大家!

好啦,HashMap系列,到这里就全部结束了,大家如果有什么不清楚的,可以留言交流!

你可能感兴趣的:(HashMap系列:put元素(不看完将后悔一生!))