HashMap 源码解读——构造函数、putval

57 阅读9分钟

HashMap中有三个构造函数,我们从构造函数开始看HashMap的代码

    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);
    }

    /**
     * Constructs an empty {@code HashMap} with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * Constructs an empty {@code HashMap} with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

这里我们可以看到它有两个参数

  • loadFactor 负载系数
  • threshold 阈值 当我们调用无参构造函数时我们可以看到
    /**
     * Constructs an empty {@code HashMap} with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

初始化的时候,修改了负载因子为loadFactor 0.75

当我们构建一个参数的构造函数时,我们需要传入一个initialCapacity(初始化容量)

/**
     * Constructs an empty {@code HashMap} with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

也是传入了DEFAULT_LOAD_FACTOR(0.75)做负载因子,然后调用两个入参的构造函数

public HashMap(int initialCapacity, float loadFactor) {
        // 判断初始化容量是不是小于0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //判断初始化容量是不是大于最大容量MAXIMUM_CAPACITY(1 << 30 即 2^30)
        //是的话,给最大值给initialCapacity
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //判断负载因子是否小于等于0
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        //给负载因子赋值
        this.loadFactor = loadFactor;
        //计算阈值
        this.threshold = tableSizeFor(initialCapacity);
    }

可以看到,上面构造函数HashMap(int initialCapacity, float loadFactor)最后调用了tableSizeFor(initialCapacity) 方法,下面就解析一下这个方法是干嘛的

static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

这里是一个静态方法,主要是把 -1 无符号右移指定位数得到 n,最后根据n,返回tableSize 在java中,负数的二进制表示与其原码不同,它是原码的反码再加 1 那么int -1 的转化为二进制为

int 在java中是32位的
所以int 1 就是:0000 0000 0000 0000 0000 0000 0000 0001
那么int 1 的反码就是 :1111 1111 1111 1111 11111 1111 1111 1110
那么int 1 的补码就是 :1111 1111 1111 1111 11111 1111 1111 1111

也就是32个1 2^32-1 解释完这个,我们来看Integer.numberOfLeadingZeros(cap - 1)方法

public static int numberOfLeadingZeros(int i) {
        // HD, Count leading 0's
        if (i <= 0)
            return i == 0 ? 32 : 0;
        int n = 31;
        // 1向左移动16位就是2^16
        if (i >= 1 << 16) { n -= 16; i >>>= 16; }
        if (i >= 1 <<  8) { n -=  8; i >>>=  8; }
        if (i >= 1 <<  4) { n -=  4; i >>>=  4; }
        if (i >= 1 <<  2) { n -=  2; i >>>=  2; }
        return n - (i >>> 1);
    }

这个方法,主要是返回当前值1前面有几个0,由于int 是32位的,所以当int = 0 时,返回32,int<0时,由于 负数由于从-1开始 是32个1,所以没有0

这个方法主要是为了返回2^n的值出来,构造tableSize


上面说完了构造函数,下面开始看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 {@code key}, or
     *         {@code null} if there was no mapping for {@code key}.
     *         (A {@code null} return can also indicate that the map
     *         previously associated {@code null} with {@code key}.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

我们可以看到,其实put方法,里面调用了putVal方法

    /**
     * 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<K,V>[] tab; Node<K,V> p; int n, i;
        // 首先在判断中把table赋值给tab,tab为空 则 调用resize方法,构建table给tab
        if ((tab = table) == null || (n = tab.length) == 0)
            // 主要触发了resize()方法,肯定会生成新的table
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 如果节点为空,放入tab[i]中 (就是当前的捅,还没有存在链表)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 如果已经有了链表
            Node<K,V> e; K k;
            // 判断key和hash是否相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 相等的话,把p赋值给e
                e = p;
                // 如果p是一个treeNode,则走putTreeVal
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 当hash和key都不相同的时候,开始遍历p这个链表
                // 定义了一个计数器,binCount
                for (int binCount = 0; ; ++binCount) {
                    // 赋值p.next给到e,判断p是不是链表的尾
                    if ((e = p.next) == null) {
                        // 是的话 直接创建一个node,放入p的next中
                        p.next = newNode(hash, key, value, null);
                        // 如果链表的长度大于8了,就会触发treeifyBin方法,生成红黑树
                        // 在treeifyBin方法里,会先判断 table的长度,是否已经超过了64,如果没有,则会先触发扩容操作
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 判断hash 和 key 是否相等,相等的话,直接跳出循环,不继续遍历了
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 判断e是否为空
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // 这里会根据onlyIfAbsent 判断是否要更新node的值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 记录这个hashMap修改了多少次 
        ++modCount;
        // 判断size是否大于阈值,是的话,进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

这里有五个参数

  • hash – hash for key
  • key – the key
  • value – the value to put
  • onlyIfAbsent – if true, don't change existing value
  • evict – if false, the table is in creation mode 首先hash这个值是通过hash(Object key) 这个方法生成的
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  • h = key.hashCode(): 首先,获取 key 的哈希码并将其存储在变量 h 中。
  • h >>> 16: 这一步是将 h 的二进制表示向右无符号移动了16位。这个操作是为了取得 h 的高位部分,以便与低位部分进行异或操作。
  • h = key.hashCode()) ^ (h >>> 16): 最后,这段代码将 h 与 h 右移16位的结果进行异或运算。 异或运算的结果会对 key 的哈希码进行混合,以获得更好的分布性和减少哈希碰撞的可能性。

putVal方法中有一个很重要的方法,就是resize()方法,这个方法用来处理hashMap数组的初始化以及扩容

    /**
     * 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<K,V>[] resize() {
        
        Node<K,V>[] oldTab = table;
        // 这里是取现有的table长度,作为旧的容器长度(也就是hashMap数组的长度)
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 把现有的阈值赋值给oldThr
        int oldThr = threshold;
        // 定义新的容器长度和阈值
        int newCap, newThr = 0;
        // 假如容器长度大于0(就是已经初始化过了)
        if (oldCap > 0) {
            // 旧容器大小已经超过MAXIMUM_CAPACITY(2^30)
            if (oldCap >= MAXIMUM_CAPACITY) {
                //取2^31 - 1 作为新的最大长度
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 这里先把oldCap向左移动1位,也就是*2 赋值给newCap (这里其实已经把cap容器扩大两倍了其实就是在这里进行了扩容操作)
            // newCap = oldCap * 2 
            // 如果newCap没有超过MAXIMUM_CAPACITY且oldCap>=DEFAULT_INITIAL_CAPACITY(16)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 则newThr = oldThr << 1 即 newThr = oldThr * 2 把阈值翻倍
                newThr = oldThr << 1; // double threshold
        }
        // 假如旧容器oldCap<=0,且oldThr > 0
        else if (oldThr > 0) // initial capacity was placed in threshold
            // 新的cap容器大小为oldThr长度
            // 这里分析一下为什么会出现这种情况,在使用new HashMap(a,b)的时候,我们的阈值,其实已经被初始化了
            newCap = oldThr;
            else {               // zero initial threshold signifies using defaults
                // 这里直接把DEFAULT_INITIAL_CAPACITY(16),赋值给了 newCap
                newCap = DEFAULT_INITIAL_CAPACITY;
                // 把计算新的阈值DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY 给newThr
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
        // 做完上面那些,我们容器newCap 和 newThr 基本就重新构建好了
        
        // 判断newThr是否为0,为0的话,初始化一下阈值 newCap * loadFactor
        // 这里直接调用hashMap()无参构造的时候,就会出现newThr == 0这种情况
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        // 把新的阈值赋值给threshold
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        // 创建一个in新的长度为newCap 的Node<K,V> 数组为newTab
        // 这里可以看到,扩容,是要创建一个新的数组,把旧数组的数据重新存过去,开销比较大
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        // 把新的数组 newTab 赋值给table
        table = newTab;
        // 如果旧的数组不为空
        if (oldTab != null) {
            // 遍历旧的数组,大小为oldCap旧的容器大小
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                // 把oldTab[j]赋值给e,如果e不为空
                // 其实这里的e就是取的数组中的链表的第一个值
                if ((e = oldTab[j]) != null) {
                    // 把旧容器oldTab[j]的值清空
                    oldTab[j] = null;
                    // 判断node下面还有没有值(是否最后一个节点)
                    if (e.next == null)
                        // 没有的话,把e对象的hash和 (newCap -1) 做与预算,算出在newTab的下标
                        // 然后放入新数组
                        newTab[e.hash & (newCap - 1)] = e;
                        // 如果取出来的是树节点,那就执行split()方法
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        // 如果取出来的值,不是最后一个节点,则遍历整个链表e
                        // hiTail,hiTail 其实就是链表的最后一个节点
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            // e.hash & oldCap 与操作,把所有0的值取出来
                            // 计算桶位置 i = (n - 1) & hash
                            // 这里其实是 看当前的数据的桶位置,是不是在oldCap之外
                            if ((e.hash & oldCap) == 0) {
                                // 如果loTail为空
                                if (loTail == null)
                                    // 把e赋值给loHead
                                    loHead = e;
                                else
                                    // 如果loTail不为空
                                    // 把e赋值给loTail.next(其实就是把e指给loTail的下一个节点)
                                    loTail.next = e;
                                // 把e赋值给loTail
                                // 这里就相当于把链表的指针向后指了一位,
                                loTail = e;
                            }
                            else {
                                // 如果e.hash & oldCap 不等于0,则收集到hiTail和hiTail里
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                            // 这里把e的下一个节点 赋值给e
                        } while ((e = next) != null);
                        // 执行完链表的遍历以后
                        if (loTail != null) {
                            // 把尾的下一个节点设为null
                            loTail.next = null;
                            // 把头节点放进oldCap下标范围内的数组中
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            // 把尾的下一个节点设为null
                            hiTail.next = null;
                            // 把头节点放进j+oldCap下标范围内的数组中
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }