HashMap源码阅读及阅读笔记

HashMap源码阅读

总结:把握住很重要的一点,HashMap内部通过数组 + 链表 或者 数组 + 红黑树来存储数据对,但实际上理想的状态是只用数组存储,源码以理想状态作为基础,当出现非理想状态(冲突时)才去考虑如何弥补。

1. HashMap内部通过许多存储桶存储数据结构,所有的存储桶由一个节点类型的数组 Node<K, V>[] table 表示,该数组的某个位置就是一个存储桶,一个存储桶可能会存储多个数据对(理想状态下一个桶存储一个数据对,这种情况下就没有hash冲突也不需要链表和红黑树了),可能以单链表存储也可能以红黑树存储(以单链表存储时节点为Node类型,以树存储时节点为TreeNode类型),当所有存储桶(数组table)的长度大于等于 MIN_TREEIFY_CAPACITY 且单链表的数据个数大于等于 TREEIFY_THRESHOLD 时,链表会转为红黑树

    static final int TREEIFY_THRESHOLD = 8;
    static final int MIN_TREEIFY_CAPACITY = 64;

2. 当不指定HashMap的初始容量的时候,会默认使用长度16为存储桶数组的长度,而此时threshold = 存储桶数组长度 * 负载因子 = 16 * 0.75 = 12,即当HashMap中存储数据对总数大于12时便会扩容,因此HashMap实际的容量可以认为是12 即 threshold;当指定HashMap的初始容量的时候(构造器中会传入参数值initialCapacity),会通过方法tableSizeFor(int cap)计算对应当前容量的存储桶数组的长度值(逻辑是比当前传入容量大的最小的2的幂,需要保证在理想状态下所有数据对都在数组中时,存储桶数组的长度足够)

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    
    // 返回比传入的参数大的最小的2的幂,比如传入6 则返回最小的2的幂,是8
    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;
    }

3. 重要源码及注释解析

    /** * The default initial capacity - MUST be a power of two. */
    // 默认的初始化hash桶数组的大小,如果不指定HashMap的容量,则默认使用长度为16的hash桶数组
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */
    // hash桶数组的最大长度是2的30次方
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /** * The load factor used when none specified in constructor. */
    // 默认的负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /** * 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. */
    // 默认的链表转为树的链表长度阈值,链表长度大于等于此值且此时hash桶数组的长度大于等于64时则转为树
    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. */
    // 默认链表转为树时hash桶数组的长度,链表长度大于等于TREEIFY_THRESHOLD且hash桶数组长度大于等于MIN_TREEIFY_CAPACITY才转为树
    static final int MIN_TREEIFY_CAPACITY = 64;

    /** * Basic hash bin node, used for most entries. (See below for * TreeNode subclass, and in LinkedHashMap for its Entry subclass.) */
    // 单向链表的节点类,还有一个红黑树的节点类
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> 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;
        }
    }

    /* ---------------- Static utilities -------------- */

    /** * Computes key.hashCode() and spreads (XORs) higher bits of hash * to lower. Because the table uses power-of-two masking, sets of * hashes that vary only in bits above the current mask will * always collide. (Among known examples are sets of Float keys * holding consecutive whole numbers in small tables.) So we * apply a transform that spreads the impact of higher bits * downward. There is a tradeoff between speed, utility, and * quality of bit-spreading. Because many common sets of hashes * are already reasonably distributed (so don't benefit from * spreading), and because we use trees to handle large sets of * collisions in bins, we just XOR some shifted bits in the * cheapest possible way to reduce systematic lossage, as well as * to incorporate impact of the highest bits that would otherwise * never be used in index calculations because of table bounds. */
    // 这里可以看到HashMap的key可以是null,如果是null默认其hash函数的结果为0
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    /** * Returns a power of two size for the given target capacity. */
    // 返回比传入的参数大的最小的2的幂,比如传入6 则返回最小的2的幂,是8
    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;
    }

    /* ---------------- Fields -------------- */

    /** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) */
    // hash桶数组,实际存储数据的数组,TreeNode类是Node类的子类的子类
    transient Node<K,V>[] table;

    /** * Holds cached entrySet(). Note that AbstractMap fields are used * for keySet() and values(). */
    // 所有key-value对组成的set集合
    transient Set<Map.Entry<K,V>> entrySet;

    /** * The number of key-value mappings contained in this map. */
    // size指的是存储的所有的数据对的数量和
    transient int size;

    /** * The number of times this HashMap has been structurally modified * Structural modifications are those that change the number of mappings in * the HashMap or otherwise modify its internal structure (e.g., * rehash). This field is used to make iterators on Collection-views of * the HashMap fail-fast. (See ConcurrentModificationException). */
    transient int modCount;

    /** * The next size value at which to resize (capacity * load factor). * * @serial */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    // 桶数组的扩容阈值,如果所有桶中存储的所有键值对(包括链表和红黑树中存储的键值对)数量大于此值便扩容,扩容会将桶数组长度翻倍,threshold = 桶数组长度 * loadFactor
    // 新的threshold = 翻倍后的桶数组长度 * loadFactor,因此threshold大小也会翻倍
    // 实际上threshold是比存储桶数组的长度小的,而且一旦存储的键值对总数大于threshold就会扩容,因此threshold也可以看做当前HashMap的容量
    int threshold;

    /** * The load factor for the hash table. * * @serial */
    // 负载因子,当前桶数组长度乘以这个负载因子就是当前扩容的阈值threshold,一般是0.75,当所有的桶中存储的所有键值对数量大于threshold就会扩容
    final float loadFactor;

    /* ---------------- Public operations -------------- */

    /** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */
    
	/** *HashMap内部通过 数组 + 链表, 数组 + 红黑树 的方式存储数据,数组 + 链表是拉链法解决Hash冲突,当链表长度过长时(超过TREEIFY_THRESHOLD时且此时存储桶数组的长度大于等于MIN_TREEIFY_CAPACITY)就将链表转为红黑树 *HashMap内部的数组默认不指定容量的时候从16长度开始,否则给定了初试容量构造器通过给定的初试容量大小initialCapacity,计算出存储桶数组的长度,逻辑是大于initialCapacity的最小的2的幂 *HashMap中存储桶数组的最大值是MAXIMUM_CAPACITY,当传入的initialCapacity大于MAXIMUM_CAPACITY时,会强制将其赋值为MAXIMUM_CAPACITY *当指定初始化容量initialCapacity时,通过initialCapacity值的大小来计算threshold(threshold是扩容的阈值,如果当前HashMap中的数据数量大于这个值则需要扩容) *扩容时存储桶数组长度会扩容为原来的两倍,因此容量也变为两倍 当前容量即扩容阈值 = 当前存储桶数组长度 * 扩容因子 *其实这个地方想明白一点就好理解了,存储桶的最佳状态是一个桶存储一个键值对,当出现hash冲突时才可能一个桶中存储多个键值对(先通过单向链表解决hash冲突,当链表长度过长时链表转为红黑树) *目的当然是尽量将键值对散列的足够分散然后分散存储,尽量保证一个桶中存储一个键值对(但是任何一个hash函数都无法保证无冲突)出现冲突就需要链表及红黑树解决了 */
	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 <tt>HashMap</tt> 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 <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /** * Constructs a new <tt>HashMap</tt> with the same mappings as the * specified <tt>Map</tt>. The <tt>HashMap</tt> is created with * default load factor (0.75) and an initial capacity sufficient to * hold the mappings in the specified <tt>Map</tt>. * * @param m the map whose mappings are to be placed in this map * @throws NullPointerException if the specified map is null */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

    /** * 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 <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) */
    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<K,V>[] tab; Node<K,V> p; int n, i;
		// table(hash桶)不会在构造器中初始化,而是在第一次存入数据对时初始化
		// 这是判断table是否已经初始化,未初始化则初始化hash桶
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
		// 存入新的键值对,从这里可以看出 HashMap中是通过函数hash()对key做hash,如下hash(key),然后将得到的值与hash桶的长度-1做&运算,(n - 1) & hash,n为hash桶的长度
		// 当不存在hash冲突时,数据对直接存入hash桶中
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
		// 当存在hash冲突时,如果该新加入节点的key与桶中此位置key为同一个对象或者值相同,则覆盖这个节点即可,否则通过拉链或者红黑树解决hash冲突
		// 由于Map中无重复的key,因此无论是桶中存储的数据对还是拉链、红黑树中存储的数据对,新加入的节点的key不可以有重复的,如果有重复的,则覆盖原来的value值
        else {
			// p为对key做hash得到的索引取得的hash桶中的节点
            Node<K,V> e; K k;
			// 如果hash桶索引处的节点与新加入的节点具有重复的key则直接覆盖该处节点的value值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
			// 否则判断hash桶索引处的节点是否为树节点,如果是树节点则通过红黑树解决hash冲突,插入新的节点,如果插入过程中发现新节点的key与已有的树中节点冲突则覆盖该处value
			// 覆盖的方法是统一赋值给节点e,在后面判断节点e是否为null,不是null则说明需要覆盖
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
			// 不是树节点,那么就是链表节点则通过拉链法即链表来解决hash冲突
            else {
				// 遍历链表中的所有节点,并记录共有多少个节点,用于判断是否大于链表转为树的阈值
                for (int binCount = 0; ; ++binCount) {
					// 如果发现p指针已经到了链表的最后一个节点(此时e指针为null),p指针在前e指针在p之后,则将新节点连接在链表末尾
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
						// 在链表末尾插入新节点后判断链表长度是否大于等于链表转为树的阈值,大于阈值则将链表转为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
					// 如果发现链表中存在和新插入的节点具有相同key值的节点,则覆盖该节点即可,退出链表遍历
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
					// 链表指针向右移动一位,p指针在前,e指针在后
                    p = e;
                }
            }
            // 通过e是否为null判断是否出现key值重复,出现则覆盖其对应的value值,如果覆盖了一个键值对,则整体键值对的数量没有增加,因此不需要判断是否需要扩容,直接return oldValue;
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
		// 如果是覆盖了某个键值对则会在上面直接返回oldValue,当没有覆盖键值对而是真的插入了新的键值对时才会到下面的逻辑
		// 如果当前hash桶数组中以及红黑树、链表中存储的所有键值对数量已经大于阈值threshold,则进行扩容
		// hash桶数组的长度总是2的幂次方,这个数组会在第一次插入键值对时进行初始化,如果不指定HashMap容量,会默认初始化为16,如果指定了容量则会初始化为大于容量的最小2的次幂的数
		// 比如传入的HashMap初始化容量是6,则会将数组初始化为长度8
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

    /** * 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;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //如果旧hash桶不为空
        if (oldCap > 0) {
            //超过hash桶的最大长度,将阀值设为最大值,并且hash桶数组的长度不再增加
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //新的hash桶的长度(被扩容后没有超过最大长度),将新容量阀值扩容为以前的2倍(桶数组每次扩容为两倍,通过公式threshold = cap * loadFactor,容量阈值也扩容为两倍)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //如果旧桶是空的且hash表阈值oldThr已经初始化过(此时oldThr会初始化为大于构造器中传入容量的最小的2的幂,所以可以直接作为桶数组长度使用)
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //如果旧hash桶为空,并且hash桶容量阈值没有初始化,那么需要初始化新的hash桶的容量和新容量阀值
        else {              
            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"})
            //初始化hash桶
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //如果旧的hash桶不为空,需要将旧的hash表里的键值对重新映射到新的hash桶中
        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)
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果是红黑树,需要进行树拆分然后映射
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { 
                    //如果是多个节点的链表,将原链表拆分为两个链表,两个链表的索引位置,一个为原索引,一个为原索引加上旧Hash桶长度的偏移量 
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //链表1
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //链表2
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //链表1存于原索引
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //链表2存于原索引加上原hash桶长度的偏移量
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

以下内容为转载 + 修改
HashMap源码分析(JDK 1.8)
一、概述
HashMap是我们在编程中遇到极其频繁、非常重要的一个集合类,如果能对HashMap做进一步的性能优化是非常有价值的而JDK 1.8做到了,所以非常有必要学习HashMap的重点源码,了解大师的手法。
二、底层数据结构

画图真的是个累活,好的画图工具很重要啊,上面这两张图分别画出了JDK 1.7、1.8底层数据结构,在JDK 1.7、1.8中都使用 了散列算法,但是在JDK 1.8中引入了红黑树,在链表的长度大于等于8并且hash桶的长度大于等于64的时候,会将链表进行树化。这里的树使用的数据结构是红黑树,红黑树是一个自平衡的二叉查找树,查找效率会从链表的o(n)降低为o(logn),效率是非常大的提高。
那为什么不将链表全部换成二叉树呢?这里主要有两个方面。
第一个是链表的结构比红黑树简单,构造红黑树要比构造链表复杂,所以在链表的节点不多的情况下,从整体的性能看来, 数组+链表+红黑树的结构不一定比数组+链表的结构性能高。
第二个是HashMap频繁的resize(扩容),扩容的时候需要重新计算节点的索引位置,也就是会将红黑树进行拆分和重组其实 这是很复杂的,这里涉及到红黑树的着色和旋转,有兴趣的可以看看红黑树的原理,这又是一个比链表结构耗时的操作,所以为链表树化设置一个阀值是非常有必要的。
三、源码分析
3.1 类结构

上图是HashMap的类结构,大家看看有个概念
3.2 类注释
我建议大家在读源码时可以先看看类注释,往往类注释会给我们一些重要的信息,这里LZ给大家总结一下。
(1)允许NULL值,NULL键
(2)不要轻易改变负载因子,负载因子过高会导致链表过长,查找键值对时间复杂度就会增高,负载因子过低会导致hash桶的 数量过多,空间复杂度会增高
(3)Hash表每次会扩容长度为以前的2倍
(4)HashMap是多线程不安全的,我在JDK1.7进行多线程put操作,之后遍历,直接死循环,CPU飙到100%,在JDK 1.8中进行多线程操作会出现节点和value值丢失,为什么JDK1.7与JDK1.8多线程操作会出现很大不同,是因为JDK 1.8的作者对resize方法进行了优化不会产生链表闭环。这也是本章的重点之一,具体的细节大家可以去查阅资料。这里我就不解释太多了
(5)尽量设置HashMap的初始容量,尤其在数据量大的时候,防止多次resize
3.3 类常量

  //默认hash桶初始长度16
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

  //hash表最大容量2的30次幂
  static final int MAXIMUM_CAPACITY = 1 << 30;

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

  //链表的数量大于等于8个并且桶的数量大于等于64时链表树化 
  static final int TREEIFY_THRESHOLD = 8;

  //hash表某个节点链表的数量小于等于6时树拆分
  static final int UNTREEIFY_THRESHOLD = 6;

  //树化时最小桶的数量
  static final int MIN_TREEIFY_CAPACITY = 64;

3.4 实例变量

  //hash桶
  transient Node<K,V>[] table;                         

  //键值对的数量
  transient int size;

  //HashMap结构修改的次数
  transient int modCount;

  //扩容的阀值,当键值对的数量超过这个阀值会产生扩容
  int threshold;

  //负载因子
  final float loadFactor;

3.5 构造函数

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

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

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

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

HashMap有4个构造函数。
hash桶没有在构造函数中初始化,而是在第一次存储键值对的时候进行初始化。 这里重点看下 tableSizeFor(initialCapacity)方法,这个方法的作用是,将你传入的initialCapacity做计算,返回一个大于等于initialCapacity 最小的2的幂次方。
所以这个操作保证无论你传入的初始化Hash桶长度参数是多少,最后hash表初始化的长度都是2的幂次方。比如你输入的是6,计算出来结果就是8。
下面贴出源码。

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

3.6 插入

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;
    //当table为空时,这里初始化table,不是通过构造函数初始化,而是在插入时通过扩容初始化,有效防止了初始化HashMap没有数据插入造成空间浪费可能造成内存泄露的情况
    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<K,V> 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<K,V>)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);
                    //当链表的长度大于等于树化阀值,并且hash桶的长度大于等于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;
            }
        }
        //map中含有旧key,返回旧值
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //map调整次数加1
    ++modCount;
    //键值对的数量达到阈值需要扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

HashMap插入跟我们平时使用时的感觉差不多,下面总结一下。
(1)插入的键值对是新键值对,如果hash表没有初始化会进行初始化,否则将键值对插入链表尾部,可能需要链表树化和 扩容
(2)插入的键值对中的key已经存在,更新键值对在put的方法里我们注意看下hash(key)方法,这是计算键值对hash值的方法,下面给出源码

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

hashCode()是一个int类型的本地方法,也就将key的hashCode无符号右移16位然后与hashCode异或从而得到hash值在putVal方法中(n - 1)& hash计算得到桶的索引位置 ,那么现在有两个疑问,为什么要计算如此计算hash值而不直接用hashCode()得到的值?为什么不用 hash % n而用(n-1)&hash?在这里插入图片描述
为什么要计算hash值,而不用hashCode()得到的值,用为通常n是很小的,而hashCode是32位,如果(n - 1)& hashCode()那么当n大于2的16次方加1,也就是65537后(n - 1)的高位数据才能与hashCode的高位数据相与,当n很小是只能使用上hashCode低 16位的数据,这会产生一个问题,既键值对在hash桶中分布不均匀,导致链表过长,而把hashCode>>>16无符号右移16位并与未移动的hashCode做异或得到的hash值与(n-1)运算,让 高16位间接的与(n - 1)参加计算,从而让键值对分布均匀,降低hash碰撞。
为什么使用(n - 1)& hash 而不使用hash% n呢?其实这两种结果是等价的,但是&的效率比%高,原因因为&运算是二进制直接运算,而计算机天生就认得二进制。下面画图说明一下

上图 hash&(n - 1)的结果是2,而其实hash%n 的结果也是2, hash&(n - 1)与hash%n的结果是等价的。
3.7 扩容

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //如果旧hash桶不为空
        if (oldCap > 0) {
            //超过hash桶的最大长度,将阀值设为最大值,并且hash桶数组的长度不再增加
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //新的hash桶的长度(被扩容后没有超过最大长度),将新容量阀值扩容为以前的2倍(桶数组每次扩容为两倍,通过公式threshold = cap * loadFactor,容量阈值也扩容为两倍)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //如果旧桶是空的且hash表阈值oldThr已经初始化过(此时oldThr会初始化为大于构造器中传入容量的最小的2的幂,所以可以直接作为桶数组长度使用)
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //如果旧hash桶为空,并且hash桶容量阈值没有初始化,那么需要初始化新的hash桶的容量和新容量阀值
        else {              
            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"})
            //初始化hash桶
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //如果旧的hash桶不为空,需要将旧的hash表里的键值对重新映射到新的hash桶中
        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)
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果是红黑树,需要进行树拆分然后映射
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { 
                    //如果是多个节点的链表,将原链表拆分为两个链表,两个链表的索引位置,一个为原索引,一个为原索引加上旧Hash桶长度的偏移量 
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //链表1
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //链表2
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //链表1存于原索引
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //链表2存于原索引加上原hash桶长度的偏移量
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

从上面代码中可以看出HashMap扩容时,正常情况下是将桶数组扩容为两倍,由于容量阈值threshold = 桶数组长度 * 负载因子,因此容量阈值也扩容为两倍
那么什么时候回产生扩容呢?
(1)初始化HashMap时,第一次进行put操作
(2)当键值对的个数大于threshold阀值时产生扩容,threshold=cap * loadFactor
上面就是HashMap扩容的源代码,我已经加上了注释,相信大家都能看懂了。总结一下,HaspMap扩容就是就是先计算 新的hash表容量cap 和新的容量阀值threshold,然后初始化一个新的hash表,将旧的键值对重新映射在新的hash表里。这里实现的细节当然 没有我说的那么简单,如果在旧的hash表里涉及到红黑树,那么在映射到新的hash表中还涉及到红黑树的拆分。
在扩容的源代码中作者有一个使用很巧妙的地方,是键值对分布更均匀,不知道读者是否有看出来。在遍历原hash桶时的 一个链表时,因为扩容后长度为原hash表的2倍,假设把扩容后的hash表分为两半,分为低位和高位,如果能把原链表的键值对, 一半放在低位,一半放在高位,这样的索引效率是最高的。那看看源码里是怎样写的。大师通过e.hash & oldCap == 0来判断, 这和e.hash & (oldCap - 1) 有什么区别呢。下面我通过画图来解释一下。

因为n是2的整次幂,二进制表示除了最高位为1外,其他低位全为0,那么e.hash & oldCap 是否等于0,取决于n对应最高位 相对于e.hash那一位是0还是1,比如说n = 16,二进制为10000,第5位为1,e.hash & oldCap 是否等于0就取决于e.hash第5 位是0还是1,这就相当于有50%的概率放在新hash表低位,50%的概率放在新hash表高位。大家应该明白了e.hash & oldCap == 0的好处与作用了吧。
其实,到这里基本上HashMap的核心内容都讲完了,相信大家对HashMap的源码有一定了解了。在源码中还有键值对的查询和删除都比较简单,这里就不在过多赘述了,对于红黑树的构造、旋转、着色,我觉得大家有兴趣可以了解一下,毕竟我们不 是HashMap的开发者,不用了解过多的细节,钻墙角。知道大致的原理即可。
3.8 清除
本来到这里就要结束了,但是LZ还是想跟大家聊一下HashMap总的clear()方法,下面贴出源码。

public void clear() {
        Node<K,V>[] tab;
        modCount++;
        if ((tab = table) != null && size > 0) {
            size = 0;
            for (int i = 0; i < tab.length; ++i)
                tab[i] = null;
        }
    }

HashMap其实这段代码特别简单,为什么贴出来呢,是因为我在看过别的博客里产生过疑问,到底是clear好还是新建一 个HashMap好。我认为clear()比新建一个HashMap好。下面从空间复杂度和时间复杂度来解释一下。
从时间角度来看,这个循环是非常简单无复杂逻辑,并不十分耗资源。而新建一个HashMap,首先他在在堆内存中年轻代中查看是否有足够空间能够存储,如果能够存储,那么创建顺利完成,但如果HashMap非常大,年轻代很难有足够的空间存储,如果老年代中有足够空间存储这个HashMap,那么jvm会将HashMap直接存储在老年代中,如果老年代中空间不够,这时候会触发一次minor gc,会产生小规模的gc停顿,如果发生minor gc之后仍不能存储HashMap,那么会发生整个堆的gc,也就是 full gc,这个gc停顿是很恐怖的。实际上的gc顺序就是这样的,并且可能发生多次minor gc和full gc,如果发现年轻代和老年代 均不能存储HashMap,那么就会触发OOM,而clear()是肯定不会触发OOM的,所以数据里特别大的情况下,千万不要创建一 个新的HashMap代替clear()方法。
从空间角度看,原HashMap虽然不用,如果数据未被清空,是不可能被jvm回收的,因为HashMap是强引用类型的,从而造成内存泄漏。所以综上所述我 是不建议新建一个HashMap代替clear()的,并且很多源码中clear()方法很常用,这就是最好的证明。
四、总结
(1)HashMap允许NULL值,NULL键
(2)不要轻易改变负载因子,负载因子过高会导致链表过长,查找键值对时间复杂度就会增高,负载因子过低会导致hash桶的数量过多,空间复杂度会增高
(3)Hash表每次会扩容长度为以前的2倍
(4)HashMap是多线程不安全的,我在JDK 1.7进行多线程put操作,之后遍历,直接死循环,CPU飙到100%,在JDK 1.8中 进行多线程操作会出现节点和value值丢失,为什么JDK1.7与JDK1.8多线程操作会出现很大不同,是因为JDK 1.8的作者对resize 方法进行了优化不会产生链表闭环。这也是本章的重点之一,具体的细节大家可以去查阅资料。这里我就不解释太多了
(5)尽量设置HashMap的初始容量,尤其在数据量大的时候,防止多次resize
(6)HashMap在JDK 1.8在做了很好性能的提升,我看到过在JDK1.7和JDK1.8get操作性能对比JDK1.8是要优于JDK 1.7的, 大家感兴趣的可以自己做个测试,所以还没有升级到JDK1.8的小伙伴赶紧的吧。
总结就把类注释的给搬过来了,其实在本篇文章中有一个知识点没有详细分析,就是HashMap在多线程不安全的原因,尤其扩 容在JDK 1.7 会产生链表闭环,因为要画很多图,我还没找到合适的工具,后期补充吧。

以上基于自己看源码以及部分文章,文章链接
https://zhuanlan.zhihu.com/p/34280652

全部评论

相关推荐

1 收藏 评论
分享
牛客网
牛客企业服务