HashMap 部分 源代码解读

59 阅读24分钟

1 HashMap

在这里插入图片描述

1.1 HashMap 1.7

下图是HashMap 在1.7 的底层实现数据结构图; 很明显,这个是数组 + 链表的数据结构 在这里插入图片描述 HashMap 1.7 的原始代码中定义的成员变量如下: 上面的 1~7 分别是: 1、HashMap 初始化时候的大小,也就是底层的数组的大小 2、HashMap 存放 Entry 的最大值 3、默认的负载因子,扩容使用 4、存放数据的数组,存放数据是 Entry 存放的 5、HashMap 里面存放的Entry 数量的大小 6、自定义的初始化容量 threshold 7、自定义的负载因子

对于负载因子,达到负载因子的时候,底层会进行 rehash ,数据赋值等操作,比较消耗性能,所以最好可以减少扩容减少扩容; 举例:给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 ,当插入的下一个元素之后即将变为 13 之后,那么就需要将当前哈希桶 16 的容量进行扩容。

put() 方法

    public V put(K key, V value) {
    	// 空 HashMap 表 的初始化,初始化值是自己定义的
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
		// key 为空,放进去一个空值
        if (key == null)
            return putForNullKey(value);
        // 到了这一步, key 是不空的。计算hashCode
        // 寻找在即将插入元素在 HashMap 中的位置
        int hash = hash(key);
        int i = indexFor(hash, table.length);
		
		// 根据哈希值进行取模运算,得到 table 里面的索引
		// 如果桶是一个链表,则进行遍历每个元素的 hashCode 和 key 是不是相等,相等就覆盖,返回原来的数值
		// 发生了哈希冲突的情况
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 找到了 key 相等,value 不相同,进行 value 的更新
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
		
		// 记录修改的次数
        modCount++;
		// 原来的 table[i] 的位置没有数据,直接进行添加即可
		// 也就是没有发生哈希冲突的情况
        addEntry(hash, key, value, i);
        return null;
    }
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 判断是不是需要进行扩容
        // 表的结点数大于扩容阈值,计算出来的索引在原来的表中不存在
        // 满足上面的条件,进行扩容操作,表的哈希桶数量变为原来的 2 倍
        // resize 进行头插法的,旧表元素转向新表元素
        if ((size >= threshold) && (null != table[bucketIndex])) {
        	// 扩容为原来的两倍
            resize(2 * table.length);
			
			// 将传递进来的当前 key 重新 hash 定位
			// 这是一个后置条件,先进行扩容然后进行结点的插入;
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
		
		// 走到这一步不需要扩容,直接正常的加入进去即可
		// 拿到上面的新 hash bucketIndex,组成 Entry 放到扩容后的桶子中;
		// 这个时候在加入的时候,原来的位置存在数据,那么就会形成链表
        createEntry(hash, key, value, bucketIndex);
    }
    
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

get() 方法

	// 根据传递的 key 进行查询
    public V get(Object key) {
    	// 传递的 key 为空,返回没有找到
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

		// 查询到返回 value ,没有查询到返回 bull 即可
        return null == entry ? null : entry.getValue();
    }
    
    // 上面的查询是不是存在 key 对应的 value 调用的函数
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
		
		// 得到计算好的 hashCode 以及桶子的索引,判断是不是需要找的数据
		// 如果是链表的话,会进行循环遍历找到需要找到的元素
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                // 查询到了将查询的结果以 Entry 的形式返回
                // 1.8 中是按照 Node 的形式保存的
                return e;
        }
        return null;
    }

取出来元素的步骤: 1、根据 key 计算出来得到 hashCode ,定位到具体的桶子中,构成哈希表中的数组有多少个元素,那么就会有多少个桶子;

2、得到计算好的 hashCode 以及桶子的索引,判断是不是需要找的数据;如果是链表的话,会进行循环遍历找到需要找到的元素

1.2 HashMap 1.8

在这里插入图片描述绘制的 HashMap 在添加元素之后的形状如下图所示: 在这里插入图片描述

在这里插入图片描述

哈希桶,HashMap

本文中: 哈希桶(哈希表中的数组) + 链表 + 红黑树 = HashMap

在 HashMap 1.7 中,如果 HashMap 存在的哈希碰撞严重,会使得链表变得很长,查询的效率显然会变得低下;所以在 HashMap 1.8 中对于此处进行了优化处理;

怎么优化的? 使用 数组 + 链表 + 红黑树; 在 1.7 中,是使用数组 + 链表实现哈希表的

1 定义的成员变量

需要注意的是:DEFAULT_INITIAL_CAPACITY(默认哈希桶容量) = 16;自定义的 HashMap 哈希桶的大小,最好是 2 的幂次方;

关于负载因子,一个是使用 HashMap 源代码提供的 0.75f ,一个是在使用构造器构造 HashMap 时候使用自己定义的负载因子,一般情况下,使用默认负载因子即可;

负载因子没有指定,默认使用的是 DEFAULT_LOAD_FACTOR = 0.75f(这个是综合了时间以及空间衡量而定的),在构造器中指定的话,就使用自己指定的负载因子 final float loadFactor;

	// 默认初始容量必须是 2 的幂,不是的话,会使用 tableSizeFor 进行优化处理,这个容量值得是哈希桶的容量,不是 k-v 键值对的数量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	// HashMap 中存放元素最大容量,最大的哈希桶数量
    static final int MAXIMUM_CAPACITY = 1 << 30;

	// 负载因子,也就是控制什么时候开始扩容,哈希桶不够的时候扩容
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

	// 转换成为 红黑树的条件,哈希碰撞之后,链表的长度是 8 
	// 就是哈希桶里面的元素和链表的元素加起来大于等于9 的时候,发生树化
	// 单独讲链表的话,链表的长度大度等于 8 的时候树化,哈希桶里面占用一个位置
    static final int TREEIFY_THRESHOLD = 8;

	// 将红黑树转换成为普通链表的条件
    static final int UNTREEIFY_THRESHOLD = 6;

	// 另一个控制转换为红黑树的条件,HashMap 里面存储的元素整体大于 64 并且单个哈希碰撞产生的链表元素个数大于等于 8 ,也就是加进去一个结点是 9 的时候会树化
    static final int MIN_TREEIFY_CAPACITY = 64;

	// Node<> 数组不参加序列化,原因后面阐述 
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     */
    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;

	// HashMpa 需要扩容的阈值,大于这个数值就会扩容,注意是大于,等于的时候是不会扩容的
	// The next size value at which to resize 
	// (capacity * load factor).
	// loadFactor 表示当前 HashMap 的负载率,默认的负载率是 0.75f 
    int threshold;

	// 使用构造函数 HashMap(int initialCapacity, float loadFactor) 传递进来的自定义 负载率,用来确定,扩容的阈值,超过阈值才会扩容,等于阈值是不会扩容的;
    final float loadFactor;
capacity哈希桶的个数也就是哈希表中数组部分的数组元素的个数,数组里面可以不存放结点,理论上可以存放的结点个数
loadFactor使用构造函数进行传递进来,或者使用默认的 0.75
thresholdcapacity * loadFactor,是扩容的阈值
sizeHashMap 当前存放的 Node 的个数;扩容判断:(++size > threshold)

2 HashMap 构造方法

没有任何参数的 HashMap 使用的初始化容量是 16 ,使用默认的负载因子 0.75f

    /**
     * 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
    }

只有一个参数 int 类型,指定初始容量大小,负载因子传递进去的话使用的幂次方,使用默认的负载因子 0.75

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

指定初始化容量,以及指定负载因子的构造函数。一般使用的很少

// 看一个参数比较全的构造函数,构造函数中并未给table分配内存空间,此构造函数HashMap(Map<? extends K, ? extends V> m)会给table分配内存空间
public HashMap(int initialCapacity, float loadFactor) {
    // 判断初始化容量是否合法,如果<0则抛出异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    // 判断initialCapacity是否大于 1<<30,如果大于则取 1<<30 = 2^30
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 判断负载因子是否合法,如果小于等于0或者isNaN,loadFactor!=loadFactor,则抛出异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
    // 赋值loadFactor
    this.loadFactor = loadFactor;
    // 通过位运算将threshold设值为最接近initialCapacity的一个2的幂次方(这里非常重要)
    // //设置阈值为  》=初始化容量的 2的n次方的值
    // 防止程序员不是使用 2 的幂次方进行 HashMap 的初始化容量设置
    this.threshold = tableSizeFor(initialCapacity);

    //新建一个哈希表,同时将另一个map m 里的所有元素加入表中
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
}

需要注意: HashEntry 修改成为 Node; 1.7 transient Entry<K,V>[] table; 1.8 transient Node<K,V>[] table;

    /**
     * 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.)
     */
    transient Node<K,V>[] table;

Node 的核心组成其实也是和 1.7 中的 HashEntry 一样,存放的都是 key value hashcode next 等数据,再来看看核心方法。

3 Node<K,V>

在 HashMap 1.8 中每一个结点中的字段的示意图如下所示:

图片名称
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//哈希值
        final K key;//key
        V value;//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; }

        // 每一个节点的hash值,是将key的hashCode 和 value 的 hashCode 异或运算得到的
        // 就是挂载到哈希表上面的每一个链表上面的 Node 的哈希值
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        // 设置Node 结点新的 value 同时返回旧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;
        }
    }

这个是挂载到哈希桶上面的单链表的每一个结点的表示; 每一个节点的哈希值是:key 和 value hashCode 异或得到的;

4 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 <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
 * <p>
 * 往哈希表里插入一个节点的putVal函数,如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value。如果evict是false。那么表示是在初始化时调用的
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {

    // Node 数组中每一个元素都是 Node ,是当前的哈希表
    // p 当做临时的链表结点
    Node<K, V>[] tab;
    Node<K, V> p;
    int n, i;

    // 如果当前哈希表是空的,代表进行初始化,存放第一个元素的时候进行初始化
    if ((tab = table) == null || (n = tab.length) == 0) {
        // 直接扩容,将扩容后的哈希桶的长度赋值给 n ,也可以理解为数组的长度
        n = (tab = resize()).length;
    }

    // (n - 1) & hash 确定元素存放在哪个桶中
    // //如果当前index = (n - 1) & hash 的节点是空的,表示没有发生哈希碰撞。 直接构建一个新节点Node,挂载在index处即可
    // 使用位运算提高运算效率
    // 向哈希表中放置元素有可能是添加的第一个元素,原来的哈希桶没有元素直接放置即可,原来的哈希桶有元素从而产生了哈希碰撞,哈希碰撞的处理有覆盖掉原来的value,放置的元素在红黑树上,放置的元素在链表上,if else 就是这几种情况的处理
    if ((p = tab[i = (n - 1) & hash]) == null) {
        // 执行下来说明桶为空,新生成一个结点放在数组中;
        tab[i] = newNode(hash, key, value, null);
    } else {  // 执行到这说明找到哈希桶里面存在元素,发生了哈希碰撞
        // 创建临时结点 e 
        Node<K, V> e;
        K k;

        // 如果哈希值相等, key 也相等,覆盖掉 value 
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
            // 将当前结点 p 的引用赋值到临时结点 e 
            e = p;
        }
        // 如果结点是红黑树的结点
        else if (p instanceof TreeNode) {   
            // 将结点放在红黑树中
            e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
        } else {// 不是上面的情况,那么这个新加进来的 Node 需要放在链表中处理
            // 遍历当前链表
            for (int binCount = 0; ; ++binCount) {
                //  p.next 是空的,说明这个 Node 是尾结点,放在链表的尾部即可
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null); // 元素插入到尾部

                    // 如果链表达到了树化阈值 8 ,也就是达到了转换为红黑树的阈值,链表的长度已经为 8 ,如果再加上哈希桶上面的一个结点,一共有 9 个结点
                    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 表示在链表循环的过程中,找到了和插入结点一样的 hash 和 key 的结点
        // 退出来找到了链表中需要覆盖的结点
        // e 非空,存在需要覆盖的结点
        if (e != null) { // existing mapping for key
            // value 进行更新
            // 则覆盖节点值,并返回原oldValue
            V oldValue = e.value;

            // 如果 e 里面的值为空或者在调用 put 函数的时候,设置了不能覆盖,那就不能覆盖掉;
            if (!onlyIfAbsent || oldValue == null) {
                e.value = value;
            }
            afterNodeAccess(e);
            // 将 value 更新后 返回 value
            return oldValue;
        }
    }

    // 记录发生在内部结构发生变化的次数
    ++modCount;


    // 判断有没有达到扩容的阈值也就是负载因子计算出来的阈值,不是转换成为红黑树的阈值
    // 插入进来 12 个的时候,因为 size++ 在第13 个元素没有插入的时候,已经扩容了, Java 7 是先扩容后面把多余的这个元素插入进去,重新 hash
    if (++size > threshold) {
        resize();
    }


    // 元素插入之后进行回调操作
    // 这是一个空实现的函数,用作LinkedHashMap重写使用
    afterNodeInsertion(evict);
    return null;
}

参考一个来自掘金上面的一张图片,描述整个 put() 添加元素的过程: 在这里插入图片描述

HashMap插入元素的另外一种流程图 在这里插入图片描述

5 resize()

初始化或加倍哈希桶大小; 如果是当前哈希桶是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() {
    	// oldTab 为当前表的哈希桶,也就是找个引用指向 table; 后面 table 需要指向新哈希桶,这里先保存着,放置旧的哈希桶找不到了
        Node<K,V>[] oldTab = table;
        //当前哈希桶的容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 当前的阈值,也就是判断是否需要进行扩容的阈值后面使用 ++size 进行判断
        // 树化和扩容是两个阈值,树化的阈值是 链表 的长度大于 8 ;
        // 扩容是哈希桶的容量大于 threshold;
        int oldThr = threshold;
        // 新表中的容量,新表需要扩容时候所需要达到的阈值
        // 刚开始的容量为 0 ,在元素加入的一瞬间进行扩容操作;
        int newCap, newThr = 0;

		// 当前哈希桶容量大于 0 
        if (oldCap > 0) {
        	// 当前哈希桶容量到达上限
            if (oldCap >= MAXIMUM_CAPACITY) {
            	// 设置扩容的阈值为 2^32 - 1
                threshold = Integer.MAX_VALUE;
                // 达到了数组中桶子的最大值,不能再大了,直接返回 2^32 - 1
                return oldTab;
            }
			// 新的数组容量是旧数组容量的二倍并且小于最大容量并且旧数组大于默认的初始容量16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 将新的哈希桶的容量扩容为原来的 2 倍,动态数组那里是 1.5 倍,这是 HashMap 两个不同的概念
                newThr = oldThr << 1; // double threshold
        }
        // 上面是考虑的旧的数组元素是 无穷大 大于 16 到无穷大,下面是考虑旧数组中原来容量在 (0,16) 的情况
		// 如果当前哈希表是空的,但是存在阈值,代表是初始化时,指定了容量,阈值的情况,使用了HashMap 执行初始化容量以及负载因子的构造函数
        else if (oldThr > 0) // initial capacity was placed in threshold
            // 新表的容量设置成为原来使用构造函数传进来的计算出来的阈值,init * loadFactor
            newCap = oldThr; // 这里没有设置阈值是因为,构造参数里面给了扩容阈值,已经有了
        // 上面将所有需要扩容的的阈值都讨论了,下面就是添加第一个元素时候的扩容机制,将数组直接扩容到 16 
        else {//如果当前表是空的,而且也没有阈值。代表是初始化时没有任何容量/阈值参数的情况  
            newCap = DEFAULT_INITIAL_CAPACITY; // 此时新表的容量为默认的 16 
            // 扩容的阈值设置成为 16 * 0.75 = 12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // //如果新的阈值是0,对应的是  当前表是空的,但是有阈值的情况
        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 数组构成的表,表里面有很多的 Node 结点,定义了 Node 数组的初始化容量
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        // 更新哈希桶的引用,新的哈希桶是扩容后的结果,里面还没有放数据,只是一个空桶子
        table = newTab;
		
	// 如果在旧的哈希桶中存在数据,
	// 下面开始将旧哈希桶中的所有结点转换到新的哈希桶中
        if (oldTab != null) {
            // 遍历老的哈希桶
            for (int j = 0; j < oldCap; ++j) {
            	// 定义一个临时的哈希桶 e ,方便后面的赋值操作
                Node<K,V> e;
                // 当前的哈希桶存在数据时,将整个哈希桶赋值给临时变量 e ,哈希桶中可能有一个链表,可能有一个结点,可能有一个红黑树;
                if ((e = oldTab[j]) != null) {
                    // 将原来的哈希桶置空,方便 GC ,清理垃圾
                    oldTab[j] = null;
                    // 这个哈希桶只有一个结点,没有发生哈希碰撞
                    if (e.next == null)
                    	// 直接放到新的哈希桶里面
                    	// 注意这里取下标 是用 哈希值 与 桶的长度-1(newCap - 1) 。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高
                        newTab[e.hash & (newCap - 1)] = e;
                    // 如果发生过哈希碰撞 ,而且是节点数超过8个,转化成了红黑树(暂且不谈 避免过于复杂, 后续专门研究一下红黑树)
                    else if (e instanceof TreeNode)
                    	// 将原来的二叉树结构分解组成新的红黑树
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // //如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
                    else { // preserve order
                    	// 下面的话有点儿绕,需要继续理解
                    	// //因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位=  low位+原哈希桶容量
                        //低位链表的头结点、尾节点
                        Node<K,V> loHead = null, loTail = null;
                        // 高位链表的头结点,尾结点
                        Node<K,V> hiHead = null, hiTail = null;
                        // 临时结点,存放 e 的下一个结点
                        // 从上面下来,进行哈希桶的拷贝赋值,到这里,e 代表一个存储链表的哈希桶,链表上的每一个节点都是 Node
                        Node<K,V> next;
                        do {
                            next = e.next;
                            // 这里又是一个利用位运算 代替常规运算的高效点: 利用哈希值 与 旧的容量,可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位
                            // 下面表示在低位 计算在新表中的位置 == 0 表示在原来的位置
                            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) { // 将低位链表存放在原index处
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) { // 将高位链表存放在新index处
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

参考技术大佬的总结: HashMap底层原代码 详解

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 1.老表的容量不为0,即老表不为空
    if (oldCap > 0) {
        // 1.1 判断老表的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,并直接返回老表,
        // 此时oldCap * 2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯的将阈值扩容到最大
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 1.2 将newCap赋值为oldCap的2倍,如果newCap<最大容量并且oldCap>=16, 则将新阈值设置为原来的两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 2.如果老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值,则将新表的容量设置为老表的阈值
    else if (oldThr > 0)
        newCap = oldThr;
    else {
        // 3.老表的容量为0, 老表的阈值为0,这种情况是没有传初始容量的new方法创建的空表,将阈值和容量设置为默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 4.如果新表的阈值为空, 则通过新的容量*负载因子获得阈值
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 5.将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表。
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 6.如果老表不为空,则需遍历所有节点,将节点赋值给新表
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {  // 将索引值为j的老表头节点赋值给e
                oldTab[j] = null; // 将老表的节点设置为空, 以便垃圾收集器回收空间
                // 7.如果e.next为空, 则代表老表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 8.如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同)
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 9.如果是普通的链表节点,则进行普通的重hash分布
                    Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
                    Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 9.1 如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
                                loHead = e; // 则将loHead赋值为第一个节点
                            else
                                loTail.next = e;    // 否则将节点添加在loTail后面
                            loTail = e; // 并将loTail赋值为新增的节点
                        }
                        // 9.2 如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
                        else {
                            if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
                                hiHead = e; // 则将hiHead赋值为第一个节点
                            else
                                hiTail.next = e;    // 否则将节点添加在hiTail后面
                            hiTail = e; // 并将hiTail赋值为新增的节点
                        }
                    } while ((e = next) != null);
                    // 10.如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点
                    // 的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 11.如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后
                    // 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    // 12.返回新表
    return newTab;
}

扩容流程图理解上述的源代码 在这里插入图片描述

6 get

HashMap 1.7 1.8 的区别以及优化

下图来自网络,侵删 在这里插入图片描述

get put 过程小结

HashMap 的扩容过程小结

为什么 HashMap 线程不安全?

在并发的情况下: 1、会发生数据的覆盖 2、在多线程下面可能发生死循环,丢失数据

所以在 并发下面,不允许使用 HahsMap ,容易产生 Bug;

为什么设置 HashMap的容量需要为 2 的幂次方?

为什么设置 HashMap的容量需要为 2 的幂次方?

主要的原因就是为了减少哈希冲突,另外一方面是为了提高运行效率

在源代码中使用 (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n;

(n - 1) & hash会保证最右边的一位使用是 1 ,减少了哈希碰撞的概率,如果最右边的 0 比较多,那么得到的散列数值的范围就会减小,那么就会增加哈希冲突;

& 运算速度快,至少比 % 取模运算快,并且 & 运算能保证索引值肯定在 capacity 中,不会超出数组长度;

这样一来,既满足了取模的需要,又能使得哈希冲突减少,和乐而不为呢?


当指定的初始容量不是 2 的幂次方的时候,JDK 8 制作的特殊处理

所谓的特殊处理就是:找到初始化容量的最近的 2 的幂次方,

比如传递初始容量是 18 ,那么距离它最近的 2 的幂次方就是 32;

比如比如传递初始容量是 56 ,那么距离它最近的 2 的幂次方就是 64;

/**
//根据期望容量cap,返回2的n次方形式的 哈希桶的实际容量 length。 返回值一般会>=cap 
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
    //cap-1后,n的二进制最右一位肯定和cap的最右一位不同,即一个为0,一个为1,例如cap=17(00010001),n=cap-1=16(00010000)
    int n = cap - 1;
    
    //n = (00010000 | 00001000) = 00011000
    n |= n >>> 1;
    
    //n = (00011000 | 00000110) = 00011110
    n |= n >>> 2;
    
    //n = (00011110 | 00000001) = 00011111
    n |= n >>> 4;
    
    //n = (00011111 | 00000000) = 00011111
    n |= n >>> 8;
    
    //n = (00011111 | 00000000) = 00011111
    n |= n >>> 16;
    
    //n = 00011111 = 31
    //n = 31 + 1 = 32, 即最终的cap = 32 = 2 的 (n=5)次方
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
    return {(n < 0) ? 1 : (n >= MAXIMUM_CAPACITY)} ? MAXIMUM_CAPACITY : n + 1;

理解上面这一行代码:上面的大花括号是作者自己加上去的方便理解

在 put 元素的时候,传递的 key 是怎么计算哈希值的?

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

首先计算出来正常的哈希值,然后与高 16 位(高 16 位向右边移动了,那么移动之后就只是剩下高 16 位)进行进行异或运算,产生最终的哈希值;

为什么是高 16 位进行异或运算? 进行进行无符号的右移操作之后,原来的高位移动到了现在的低位,使得高位低位同时加入到了哈希值的计算,使得计算出来的结果的随机性得到了增加,可以减少哈希碰撞的可能性;

HashMap 线程不安全如何解决?有没有线程安全的并发容器?

2 ConcurrentHashMap

在这里插入图片描述

ConcurrentHashMap 1.7

CuncurrentHashMap 1.7 实现原理

jdk7:数据结构:ReentrantLock+Segment+HashEntry 一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构 元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部锁:Segment分段锁 Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segment get方法无需加锁,volatile保证

ConcurrentHashMap 1.8

CuncurrentHashMap 1.8 实现原理

jdk8: 数据结构:synchronized+CAS+Node+红黑树 Node的val和next都用volatile修饰,保证可见性查找,替换,赋值操作都使用CAS锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容 读操作无锁: Node的val和next使用volatile修饰,读写线程对该变量互相可见 数组用volatile修饰,保证扩容时被读线程感知

ConcurrentHashMap 1.7、1.8 实现有何不同?为什么这么做?

3、总结 从HashMap 到 CurrentHashMap发展历程

HashMap 在JDK 1.7 的时候,当 Hash 冲突严重时,在哈希桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。

JDK1.8的HashMap解决了java7中的HashMap死循环的问题(JDK7头插法引起的问题,考虑头插法的原因是不用遍历链表,提高插入性能,但在JDK8已经改为尾插法了,不存在这个问题),但是HashMap在多线程下面还是会发生数据覆盖,所以在多线程下,HashMap还是不能用,需要使用ConcurrentHashMap,保证多线程的安全;

HashMap1.8 使用红黑树解决此问题 关于红黑树的扩容:在java1.8中,如果链表的长度大于8并且桶的数量必须大于64,那么链表将转换为红黑树;

参考博客

详细的 HashMap 源代码解读 目前发现写的最好的

另外一篇讲的不错的HashMap

按照面试形式回答的HashMap 回答的比较简练 美团面试里面询问到的 HashMap