【Java源码分析】Java8的HashMap源码分析

Java8中的HashMap源码分析


  • 源码分析

    1. HashMap的定义
    2. 字段属性
    3. 构造函数
    4. hash函数
    5. comparableClassFor,compareComparables函数
    6. tableSizeFor函数
    7. putMapEntries,putAll函数
    8. size,isEmpty函数
    9. containsKey,containsValue函数
    10. resize函数(扩容机制)
    11. treeifyBin函数
    12. get,getNode,getOrDefault函数
    13. put,putVal,putIfAbsent函数
    14. remove,removeNode,clear函数
    15. replace函数
    16. compute,computeIfAbsent,computeIfPresent函数
    17. values,keySet,entrySet函数
    18. merge函数
    19. forEach函数
    20. replaceAll函数
    21. clone函数
    22. loadFactor函数
    23. capacity函数
    24. writeObject,readObject函数
    25. internalWriteEntries函数
    26. newNode,newTreeNode,replacementNode,replacementTreeNode函数
    27. afterNodeAccess,afterNodeInsertion,afterNodeRemoval函数
  • 内部类分析

    1. KeySet
    2. Values
    3. EntrySet
    4. HashIterator
    5. KeyIterator
    6. ValueIterator
    7. EntryIterator
    8. Node<K,V>
    9. HashMapSpliterator<K,V>
    10. KeySpliterator<K,V>
    11. ValueSpliterator<K,V>
    12. EntrySpliterator<K,V>
    13. TreeNode<K,V>
  • 一些问题

结构分析


结构

在这里插入图片描述
图片来源于 @作者: 美团技术团队

  • HashMap的数据结构基础就是一张哈希表,使用拉链法来解决哈希冲突
  • HashMap的Java语言实现基础数组 + (链表 or 红黑树

Java层面分析
  • HashMap的table数组就是一个位桶,它索引就是(key.hash mod length)存放的地址,该索引所存放的对象就是Node<K,V>节点
  • 既HashMap的最小单位就是一个Node节点,整个存储结构就是一个Node数组 + (链表 or 红黑树)
  • key.hash mod length的位运算优化就是 key.hash & length - 1,相与运算

特点
  • 允许键/值为空对象(null)
  • 非线程安全
  • 无序,无保证顺序,且不保证顺序不随时间而变化

源码分析


1 - HashMap的定义
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {}
  • HashMap是一个泛型类
  • HashMap继承于AbstractMap,实现了Map,Cloneable,Serializable接口
  • Cloneable是一个标记接口,实现Cloneable代表HashMap会去提供Clone方法,遵循原型设计模式

2 - 字段属性
	//序列化Id,版本号
    private static final long serialVersionUID = 362498820763181265L; 
	//默认Map容量为16,移位运算,向左移4位,所以是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大容量,向左移30位,1073741824,Integer最大为1073741824
    static final int MAXIMUM_CAPACITY = 1 << 30;
	//默认的填充因子,0.75倍
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
	//当桶(bucket)上的结点数大于这个值时会转成红黑树
    static final int TREEIFY_THRESHOLD = 8;
   	//当桶(bucket)上的结点数小于这个值时红黑树将还原回链表
    static final int UNTREEIFY_THRESHOLD = 6;
	//桶中结构转化为红黑树对应的数组的最小大小,如果当前容量小于它,就不会将链表转化为红黑树,而是用resize()代替	
    static final int MIN_TREEIFY_CAPACITY = 64;
    //存储元素的数组,大小总是2的幂
	transient java.util.HashMap.Node<K,V>[] table;
	//存放具体元素的集合
    transient Set<Map.Entry<K,V>> entrySet;
    //集合目前的大小,并非数组的length,即当前Map存储了多少个元素
    transient int size;
	//计数器,版本号,每次修改就会+1
    transient int modCount;
  	//临界值,当实际节点个数超过临界值(容量*填充因子)时,就会进行扩容
    int threshold;
 	//填充因子
    final float loadFactor;
  • 移位运算,1 << 4 ,就等于00000001 向左移动4位,00010000,所以是16
  • JDK1.8引入了红黑树结构,因哈希冲突导致链表长度超过8时,会将链表转换成红黑树
  • 重点留意填充因子临界值等概念
  • HashMap的数组容量大小只能是2的幂次方整数
  • length > threshold就会扩容,threshold = table.length*loadFactor
  • 在HashMap中,哈希桶数组table的长度length大小必须为2的n次方(一定是合数),这是一种非常规的设计,常规的设计是把桶的大小设计为素数。相对来说素数导致冲突的概率要小于合数;同时也是使用位运算来优化hash%len的前提

3 - 构造函数
	/**
     * HashMap带参构造函数
     * 自定义初始容量和加载因子
     *
     * @param initialCapacity 初始容量
     * @param loadFactor 填充因子
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)                          //传入的初始容量值小于0,抛异常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)         //传入的初始容量值大于Integer最大值,补偿措施
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))  //填充因子有问题,抛异常
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
        this.loadFactor = loadFactor;                    //为填充因子赋值
        this.threshold = tableSizeFor(initialCapacity);  //为传入的初始容量做处理后,
        //tableSizeFor是一个精妙的算法,主要功能是返回一个比给定整数大且最接近的2的幂次方的整数
        //因为传入的initialCapacity不一定是2的幂次方
    }

    /**
     * HashMap带参构造函数
     * 自定义初始容量
     * @param initialCapacity
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * HashMap无参构造函数
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /**
     * HashMap有参构造函数
     * 参数是另一个map,作用是将Map的实现类构造成一个HashMap
     * 也可以理解为是拷贝
     * @param m Map的实现类实例
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        //将m中的所有元素添加至HashMap中
        putMapEntries(m, false);
    }
  • 4种构造函数,1个无参,3个有参
  • 我们可以直接无参构造HashMap,初始容量和填充因子使用默认值
  • 我们也可以传入初始容量,使用默认填充因子构造HashMap
  • 也可以都不使用默认值,手动传入初始容量和填充因子
  • 当然我们还可以从其他Map实现中拷贝其元素来构造我们的HashMap


4 - hash函数
    static final int hash(Object key) {
        int h;
        /**
         * 1. key为null,则hash值为0
         * 2. key不为null,执行key的hashcode方法(得到的hashcode值需要进行异或计算)
         */
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        /**
         * 1. h = key.hashCode() 第一步:取hashcode值  
         * 2. h^(h>>>16)         第二步:高位参与运算
         * 3. (n - 1) & hash     第三步: 结果对table的数组长度进行取模,得到具体的存储索引位置  
         */
    }
  • hash算法是HashMap的重点,可以看到传入参数是元素的key,所以必须要求key对象的类重写了hashcode方法

  • 异或计算两个二进制值进行计算,比如000111,10000进行异或,则是100111。异或是指同位的值不相等,比如一个是1,一个是0

  • 这里对hashcode得到的h转换成2进制,然后向右移16位,数值小的情况下,一般都是000000000…,与0异或等于不需要异或。所以很多情况下是没有差别的

  • 为什么要对得到的hashcode值进行异或计算?加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突

  • 为什么需要取模运算,因为进行二次计算的值可能不在数组的索引范围,所以结果需要对数组长度进行mod运算,得到具体的数组索引位置,实际应用中使用hashcode与数组长度-1进行与操作(效果等于hash%len)。

  • Java 8 中的取模运算不集成在hash方法中,取模运算出现在真正需要用到计算数组的索引位置时用到,比如put方法,resize方法中

  • 想要了解"hash >> 16"扰动函数的,拉到最后面的相关问题,有解释


5 - comparableClassFor,compareComparables函数
	 /**
     * 为了查看对象x的Class是否实现了Comparable接口
     * 
     * @param x 要检查的对象
     * @return
     */
    static Class<?> comparableClassFor(Object x) {
        if (x instanceof Comparable) {  //x是否是Comparable的实现类
            Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
            if ((c = x.getClass()) == String.class) // bypass checks,如果x是String类型,返回c
                return c;
            if ((ts = c.getGenericInterfaces()) != null) { //如果获取的Type数组不为null,则遍历泛型接口数组
                for (int i = 0; i < ts.length; ++i) {
                    if (((t = ts[i]) instanceof ParameterizedType) &&  //如果取得的Type具体是ParameterizedType类型
                            ((p = (ParameterizedType)t).getRawType() == //且该泛型接口的原生类型是Comparable
                                    Comparable.class) &&                //且...就返回c,c就是x的类类型,即Class类型
                            (as = p.getActualTypeArguments()) != null &&
                            as.length == 1 && as[0] == c) // type arg is c
                        return c;
                }
            }
        }
        return null;
    }


    @SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
    static int compareComparables(Class<?> kc, Object k, Object x) {
        return (x == null || x.getClass() != kc ? 0 :
                ((Comparable)k).compareTo(x));
    }

6 - tableSizeFor函数
	/**
     * tableSizeFor函数的作用是保证传入的cap参数被处理后是2的幂次方
     * 即得到大于等于initialCapacity的最小的2的幂次方
     * 
     * @param cap cap是HashMap实例化时传入的初始容量
     * @return
     */
    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;
    }
  • >>>是无符号向右移位运算,|是二进制或运算,只有一方有1,结果就是1

  • 为什么要做cap - 1的操作?因为如果cap已经是2的幂次方,执行完无符号右移操作之后,返回的capacity将是这个cap的2倍,这是为了防止cap已经是2的幂次方的情况

  • 说白了,tableSizeFor的核心就是判断cap是否合理,既是2的幂次方,且不超过MAXIMUM_CAPACITY。如果不是2的幂次方,那么会不断通过巧妙的位运算,将原cap的位置1,最后+1,得到一个2的幂次方的偶数

  • HashMap源码注解 之 静态工具方法hash()、tableSizeFor()(四) 如果对这里的算法有兴趣的,可以看这篇文章,里面有张图说明的很清晰


7 - putMapEntries,putAll函数
	/**
	 * default方法,不对外公开,只允许构造函数和putAll内部调用,有两种模式
     * putMapEntries的作用:将别的集合元素填充到当前集合中,可以看做是一个拷贝函数
     * 该方法有两个参数m和evict,evict主要在putVal得到使用
     * @param m 参数集合
     * @param evict evict为false则代表创建模式,用于HashMap的构造。如果为true,则用于putAll
     */
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();   //获取参数集合m的大小
        if (s > 0) {        //如果参数集合有元素,即大小不为0,则进入下一轮判断
        	//如果当前集合的table为空,即当前集合是空集合,则初始化参数
            if (table == null) { // pre-size  
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?  //判断得到的ft是否小于集合最大可支持的容量,如果是返回ft
                        (int)ft : MAXIMUM_CAPACITY);       //不是则返回最大的可支持容量MAXIMUM_CAPACITY(1<<30)
                if (t > threshold)                           //如果t大于临界值(容量*填充因子)
                    threshold = tableSizeFor(t);             //则对t进行是否是2的幂次方的检查并修正
            }
            
            //如果当前集合table不为空的情况下,且参数集合的大小大于当前集合的临界值,则扩容
            else if (s > 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);
            }
        }
    }
	
   /**
	* 公有方法,将集合m的所有元素填充到当前集合中
	* 实际调用的是putMapEntries方法,但是evict值为true,即代表不是创建模式,即非通过构造函数调用
	* 其实就是putMapEntries方法的对外公开模式
	*/
	public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);  //evict 为true
    }
  • 重点是resize()扩容方法和putVal()方法
  • putMapEntries是对内使用的填充方法,putAll是其对外公开版本

8 - size,isEmpty函数
	//获得集合的大小
    public int size() {
        return size;
    }

	//是否是空集合
    public boolean isEmpty() {
        return size == 0;
    }
  • 没什么好说的啦…

9 - containsKey,containsValue函数
	/**
     * 公有方法,判断HashMap中是否有该key
     * 本质是通过getNode方法获取节点,只要有该节点则代表存在该Key
     * @param key
     * @return
     */
	public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }

	/**
     * 公有方法,判断HashMap中是否有该value
     * @param value
     * @return
     */
    public boolean containsValue(Object value) {
        java.util.HashMap.Node<K,V>[] tab; V v;  //获取临时table
        if ((tab = table) != null && size > 0) {   //如果table不等于空且集合大小不为0
            
            //下面的语句就是循环整个Hash表,遍历table以及存储在链表的里面的节点
            for (int i = 0; i < tab.length; ++i) { //循环table的大小
                for (java.util.HashMap.Node<K,V> e = tab[i]; e != null; e = e.next) {  //循环每个table节点的链表节点
                    //每次大循环e等于table[i],如果该table节点不为空,则执行下面的语句,然后e指向e在链表中的下一个节点(非table)
                    if ((v = e.value) == value ||                 //比较节点的Value是否等于参数Value
                            (value != null && value.equals(v)))
                        return true;                             //如果存在相同则返回true
                }
            }
        }
        return false;  //如果执行到这一步,则代表整个hash表都没有找到该value
    }

10 - 扩容机制 - resize函数
 final java.util.HashMap.Node<K, V>[] resize() {
        java.util.HashMap.Node<K, V>[] oldTab = table;       //获取旧table数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;   //获取旧table数组长度
        int oldThr = threshold;                              //获取旧临界值
        int newCap, newThr = 0;                              //初始化新容量和新临界值的临时变量

        /**
         * 这个阶段在计算获取数组新容量和新临界值
         */
        //如果旧数组的长度大于0
        if (oldCap > 0) {
            //如果旧数组的长度大于等于HashMap的最大容量限制
            if (oldCap >= MAXIMUM_CAPACITY) {
                //则临界值为Integer.MAX_VALUE,并返回旧数组,说明该数组已经达到最大长度限制,以后不会再扩容了
                threshold = Integer.MAX_VALUE;
                return oldTab;
            //如果旧数组的长度大于16且长度的2倍仍然小于HashMap的最大容量限制,则新临界值等于旧临界值的2倍
            } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        //如果旧数组长度小于等于0,且旧临界值大于0 ,则初始化数组容量为旧临界值
        } else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //如果旧数组长度小于等于0,且旧临界值也小于等于0,则初始化容量和临界值
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;  //默认16
            newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //默认16*0.75
        }

        //如果新临界值等于0,初始化新临界值
        if (newThr == 0) {
            //ft等于容量*加载因子
            float ft = (float) newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
                    (int) ft : Integer.MAX_VALUE);
        }
        //将临时变量新临界值赋值给成员变量临界值
        threshold = newThr;

        /**
         * 这个阶段在初始化新位桶数组
         */
        //初始化新位桶数组newTab,长度为newCap,并没有拷贝数据
        @SuppressWarnings({"rawtypes", "unchecked"})
        java.util.HashMap.Node<K, V>[] newTab = (java.util.HashMap.Node<K, V>[]) new java.util.HashMap.Node[newCap];
        //成员变量table指向新数组
        table = newTab;

        /**
         * 这个阶段在拷贝数据
         */
        //如果旧数组不为空
        if (oldTab != null) {
            //遍历旧数组
            for (int j = 0; j < oldCap; ++j) {
                java.util.HashMap.Node<K, V> e;

                //首先判断位桶的所有首节点
                //如果位桶数组的节点(不涉及链表)不为空
                if ((e = oldTab[j]) != null) {
                    //则释放旧节点数据,指向null,是为了虚拟机回收旧数组外壳
                    //当遍历完毕后,旧数组的所有元素不再指向任何对象
                    oldTab[j] = null;

                    //其次判断位桶首节点的后继结构
                    //如果当前节点的下一个节点(链表or红黑树)为空,则代表该位置没有哈希冲突
                    if (e.next == null)
                        //则在新数组的e.hash & (newCap - 1)位置指向e
                        //e.hash & (newCap - 1)的意思是用hashcode于数组长度-1进行mod运算,求出e节点在新数组中的索引位置
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果当前节点的下一个节点不为空,且是红黑树节点,执行红黑数的方法
                    else if (e instanceof java.util.HashMap.TreeNode)
                        ((java.util.HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                    //如果当前节点的下一个节点不为空,且不是红黑树节点,则是链表结构
                    //链表优化重hash的代码块,这里是对Java 7中重新hash计算新索引位置的优化代码
                    else { // preserve order
                        java.util.HashMap.Node<K, V> loHead = null, loTail = null;
                        java.util.HashMap.Node<K, V> hiHead = null, hiTail = null;
                        java.util.HashMap.Node<K, V> next;
                        do {
                            //next是首节点的直接后继节点
                            next = e.next;
                            //如果扩容后,容量二进制结构中新增的那一位对应旧索引的位置的值是0
                            //那么索引位置的数据在新数组中的索引不变
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            //如果扩容后,容量二进制结构中新增的那一位对应旧索引的位置的值是1
                            //则新索引是原索引+oldCap
                            } else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 原索引放到bucket里
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 原索引+oldCap放到bucket里
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
  • 我们可以分成3个大模块:
    1. 计算数组扩容后的新容量和新临界值
    2. 根据新容量初始化新数组,即新位桶
    3. 将旧数组中的元素取出,拷贝至新数组
  • HashMap的初始容量是16,但是一开始的时候如果是new HashMap<>(),没有初始化数组大小的话,则会在put等操作时触发resize方法,赋予16大小的初始容量。
  • 位桶数组的扩容是通过位运算实现的,向左移动1位,意义等同于乘2,所以HashMap每次扩容都是原来的2倍,且容量都是2的倍数。
  • 因为元素在位桶数组中的索引位置是根据hash & len - 1(效果等于hash%len)来获取的,所以扩容后,新长度发生变化,数据往新数组迁移的过程是需要重新计算元素在新数组中索引的,即rehash。但JDK 1.8对rehash的部门进行了算法优化
  • 对于拷贝节点JDK1.8相对1.7的优化部分,美团大佬分享的文章更尽其详,我们可以在resize章节有看到更多接受和图文描述

11 - treeifyBin函数

12 - get,getNode,getOrDefault函数
	/**
     * 公有方法,通过Key获取Value
     * 本质是通过getNode方法获取
     *
     * @param key
     * @return
     */
    public V get(Object key) {
        java.util.HashMap.Node<K,V> e;
        //如果获取的Node节点为null,则返回空,如果不为空则返回节点.value
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * default方法,不对外公开,只对包下的类开放使用
     * 通过hash和key获取对应的Node节点,返回Node节点
     *
     * @param hash
     * @param key
     * @return
     */
    final java.util.HashMap.Node<K,V> getNode(int hash, Object key) {
        java.util.HashMap.Node<K,V>[] tab; java.util.HashMap.Node<K,V> first, e; int n; K k;
        //集合table不为空,且table的大小大于0,且...不太懂,则进入下一步
        if ((tab = table) != null && (n = tab.length) > 0 && 
                //(first = tab[(n - 1) & hash]) 实际是first = tab[hash % n]的优化
                //first是这个hash地址位置在table中的链表的首节点
                //即根据key的hash值来计算得出这个key放在了hash桶数组的哪个位置上
                (first = tab[(n - 1) & hash]) != null) {
            
            //如果链表首节点的hash等于传入的hash,则进入下一步,因为整条链表的hash都一样(哈希冲突)
            if (first.hash == hash && // always check first node   
                    ((k = first.key) == key || (key != null && key.equals(k)))) //如果(首节点的key的地址等于参数key)或(参数key不为null且值与首节点key相同)
                return first;        //则代表首节点即使要找的node,返回首节点
            //如果首节点的下一个节点不为空,则进入下一步
            if ((e = first.next) != null) {
                //如果首节点是树形节点,则通过getTreeNode方法去查找(红黑树结构)
                if (first instanceof java.util.HashMap.TreeNode)
                    return ((java.util.HashMap.TreeNode<K,V>)first).getTreeNode(hash, key); 
                //如果不是树形节点,则执行下面方法
                do {
                    if (e.hash == hash &&  //如果hash对的上,且key对的上
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;         //则返回该节点
                } while ((e = e.next) != null); //遍历链表,只要节点不为空则继续,直到找到对应的Node
            }
        }
        return null; //要是执行到这步,则代表不存在Key为参数key的节点
    }
    
	/**
     * 公有方法:get()方法提供默认值的版本
     * @param key
     * @param defaultValue
     * @return
     */
    @Override
    public V getOrDefault(Object key, V defaultValue) {
        java.util.HashMap.Node<K, V> e;
        //如果hashMap中没有对应的节点,就返回defaulValue值,如果不为空则返回节点的值
        return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
    }
  • getOrDefault() is Overrides of JDK8 Map extension method
  • getNode的步骤是:
    1. 首先根据hash确定在table的位置(即hash桶的位置)
    2. 判断首节点是否要找的key
    3. 判断链表是否已转换成红黑树结构,如果是则调用红黑树查询函数,如果不是则继续
    4. 如果不是红黑树结构则遍历链表寻找对应Key的Node
  • (first = tab[(n - 1) & hash])实际是first = tab[hash % n]的优化 模运算的优化 - @作者:frapples
  • if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))比较函数是先比较hash和引用,最后比较值

13 - put,putVal,putIfAbsent函数
	/**
     * 公有方法,为集合插入一个key-value元素
     * 如果存在同一的key,则更新Value,因为传入putVal的参数onlyIfAbsent为false
     * 本质调用的是putVal方法
     *
     * @param key
     * @param value
     * @return
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * default方法,为集合插入key-value元素
     * 为实现Map.put方法和其相关的方法
     *
     * @param hash
     * @param key
     * @param value
     * @param onlyIfAbsent 如果为true,则不会覆盖旧值,即是否替换Value的flag
     * @param evict evict为false,则代表是创造模式,比如构造函数实例化HashMap就是创造模式(creation mode)
     * @return 如果key相同,新value会覆盖旧value,且返回旧value
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        java.util.HashMap.Node<K,V>[] tab; java.util.HashMap.Node<K,V> p; int n, i;
        //如果哈希表table为空或表长度为0,则初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length; //
		
		//1.
        //计算新元素的Key的hash值在table中的位置,即定位hash桶,如果计算得出的记过,即得到位置仍然指向null
        //说明该位置还没有元素,那么就将新元素节点存放到该位置,也不需要管链表或红黑树了
        //临时节点p指向新节点hash的位置的首节点
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null); //则table的i位置指向新节点
        //2.    
        //如果该位置已经有节点了,说明哈希冲突了,看是链表还是红黑树结构
        else {
            java.util.HashMap.Node<K,V> e; K k; //e是临时节点
            //首节点是否与新节点元素相同,通过比较算法比较,hash,引用,值依次比较
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p; //如果插入元素与首节点相等,临时节点e指向首节点
            //如果不相等,则判断是否是红黑树结构,是则调用树形结构的putTreeVal方法
            else if (p instanceof java.util.HashMap.TreeNode)
                e = ((java.util.HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //如果新节点不等于首节点,且table当前位置不是红黑树结构,则以链表结构计算
            else {
            	//3.
            	//在链表上遍历找到尾节点,在尾节点的next位置存放新节点
                for (int binCount = 0; ; ++binCount) {
                	//临时节点每次循环指向节点的下一个,第一次是首节点的next
                	//只要当前节点的next指向null,则就在该next上存放新节点元素,newNode
                    if ((e = p.next) == null) { 
                        p.next = newNode(hash, key, value, null);
                        //如果循环的次数>=8,即链表长度大于等于8时,执行链表转换成红黑树结构的方法
       					 //为什么这里需要TREEIFY_THRESHOLD - 1 = 7,是因为binCount是从0开始算起的
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 
                            treeifyBin(tab, hash);
                        break; //退出for循环,此时的e指向Null
                    }
                    //如果寻找尾节点期间,某个节点不为null,且跟新元素一样,说明集合已经有这个元素,则
                    //退出添加node的循环,即for循环
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e; //在还没有找到存放新节点的具体位置时,我们每次遍历都需要让p指向当前的e
                }
            }
            //4.
            //新值替换旧值
            //在寻找尾节点期间,发现有相同元素,打破循环会跳到这步,此时的e肯定不是null
            //如果是插入新节点后,打破循环,此时的e指向的是Null,所以不会执行下面的方法
            if (e != null) { // existing mapping for key
                V oldValue = e.value;  //获取旧节点的值
                if (!onlyIfAbsent || oldValue == null) //如果onlyIfAbsent为false或oldV为null
                    e.value = value; //用新值替换旧值
                afterNodeAccess(e);
                return oldValue; //返回旧值
            }
        }
        
        //每次修改集合,版本号+1,更新旧值不会触发下面操作,也不会更改版本号
        ++modCount;            
        //如果集合容量大于临界值,则扩容
        if (++size > threshold) 
            resize();           //扩容
        afterNodeInsertion(evict);
        return null;
    }
    
	/**
     * put()的不覆盖旧值版本
     * @param key
     * @param value
     * @return
     */
    @Override
    public V putIfAbsent(K key, V value) {
        //实际调用的是putVal,与put()唯一的不同是onlyIfAbsent是true
        //意思就是只有当hashMap没有改key对应的节点时才插入,如果已经存在则什么都不做
        return putVal(hash(key), key, value, true, true);
    }

 
  • putIfAbsent() is Overrides of JDK8 Map extension method

  • putVal的步骤:

    1. 判断是否是空集合,如果是初始化数组长度
    2. 不为空,则获取新元素要插入的位置,即在table中的位置
    3. 如果该位置上没有其他元素,则直接把新元素放在该位置上
    4. 如果该位置上已经有其他元素了,则新元素跟该位置上的元素进行比较,是否是同一个元素
    5. 如果是直接更新value,如果不是则判断该位置的数据结构是链表还是红黑树
    6. 如果是红黑树则执行红黑树的putTreeVal方法,如果是链表则循环链表找到尾节点
    7. 如果尾节点的下一个节点是null,就直接在next上存放新元素,插完节点后还要判断链表长度是否大于等于8,看是否要将链表转换成红黑树,最后打破循环,最后return null
    8. 在找尾节点的过程中,如果其中一个节点跟新元素是同一元素,即key相等,则打破循环
    9. 在寻找尾节点过程中,发现有相同元素而打破for循环,会执行新值替换旧值方法,返回旧值
  • 图文如下
    在这里插入图片描述
    上图片来源于 @掘金 作者:Carson_Ho
    在这里插入图片描述
    上图来源于美图技术分享 @作者: 美团技术团队


14 - remove,removeNode,clear函数
	/**
     * 公有方法,删除集合中键值为key的值,并返回删除的值
     * 实际执行的是removeNode方法
     * @param key
     * @return
     */
    public V remove(Object key) {
        java.util.HashMap.Node<K, V> e;
        //删除键值为key的节点,不匹配value值,且删除节点时会移动其他节点
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
    }

    /**
     * 默认方法:删除节点,返回对应的节点
     * @param hash  key的hash值
     * @param key   要删除节点的key
     * @param value 要删除节点的value
     * @param matchValue 如果是true,当传入的参数value与该位置的value相同时才删除,即value匹配才删除
     * @param movable    如果是true,则删除节点的时候,会移动其他节点,貌似只会对红黑树产生影响
     * @return   返回删除的节点
     */
    final java.util.HashMap.Node<K, V> removeNode(int hash, Object key, Object value,
                                                  boolean matchValue, boolean movable) {
        //tap是临时table数组,n是临时数组长度
        //p是临时节点,index是hashcode取模运算后得出的在数组中的索引位置
        java.util.HashMap.Node<K, V>[] tab;
        java.util.HashMap.Node<K, V> p;
        int n, index;
        
        /**
         * 第一步:获取参数key的hash,判断位桶是否存在(n - 1) & hash索引的节点
         */
        //如果位桶数组tap不等于null且数组长度n大于0,且该key对应的节点p不指向null,则存在对应索引位置的节点
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
            //node和e是临时节点,node用来指向最后找到的删除节点
            //k和v是key和value的临时变量
            java.util.HashMap.Node<K, V> node = null, e;
            K k;
            V v;
            
            /**
             * 第二步:知道了位桶索引,我们就需要遍历链表或者红黑树,查找key对应的具体位置
             */
            //首先判断首节点是否符合
            //如果该索引位置的首节点的hash值等于参数hash且key值相等,或参数Key不为空,且key相等
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                //node指向要被删除的节点
                node = p;
            //其次判断后继节点是否符合   
            //如果不满足上面任意条件,且位桶首节点的下一个节点不为空的情况下,即存在hash冲突时,遍历链表或者红黑树
            else if ((e = p.next) != null) {
                //如果数据结构是红黑树结构,则通过红黑树方法获取对应节点位置,赋值给node
                if (p instanceof java.util.HashMap.TreeNode)
                    node = ((java.util.HashMap.TreeNode<K, V>) p).getTreeNode(hash, key);
                //如果数据结构是链表,则遍历链表
                else {
                    do {
                        //首先判断key的hash是否相同且key的地址是否相同,或key不等于null且key的equals是否相同
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
                            //如果是则找到对应节点,赋值给node
                            node = e;
                            break;
                        }
                        //如果不相同,则p记录当前不匹配的节点,其实p在这里就是记录匹配节点的前一个节点,留给第三步使用
                        p = e;
                        //遍历下一个
                    } while ((e = e.next) != null);
                }
            }

            /**
             * 第三步: 当找到了删除节点,根据方法参数中matchValue和value的不同,执行不同的删除逻辑
             */
            //当找到删除节点时,即删除节点node不为nul 且(不需要匹配value 或 value地址相同 或 value不为null且equals相等) 的情况下
            if (node != null && (!matchValue || (v = node.value) == value ||
                    (value != null && value.equals(v)))) {
                //如果node节点是树形节点
                if (node instanceof java.util.HashMap.TreeNode)
                    //通过红黑树的删除方法删除节点(movable:删除节点时是否移动其他节点)
                    ((java.util.HashMap.TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
                //如果是链表节点
                //如果出现node == p,则代表没有执行过p == e操作,这种可能只有可能是删除节点是首节点
                else if (node == p)
                    //所以即使删除的节点是首节点,则数组的首节点索引位置要指向删除节点(首节点)的下一个节点
                    tab[index] = node.next;
                //如果删除节点不是首节点,则p的下一个节点应该指向删除节点的下一节点(p在第二步时,被用于记录删掉节点的前一节点地址)
                else
                    p.next = node.next;
                //每次发生修改,版本号+1
                ++modCount;
                //hashmap大小-1
                --size;
                //删除节点后续操作
                afterNodeRemoval(node);
                //返回删除节点
                return node;
            }
        }
        //没有匹配节点时,不删除,返回null
        return null;
    }

	/**
     * 公有方法:清空hashMap所有元素
     * 不过这种方式仅仅是让数组清空数据。
     * 数组的长度,临界值等依旧
     */
    public void clear() {
        java.util.HashMap.Node<K, V>[] tab;
        //清空hashMap必然发生改变,所以版本+1
        modCount++;
        //如果位桶数组不等于null,且有数据
        if ((tab = table) != null && size > 0) {
            //则size = 0,每个索引所存放的对象都改为null
            size = 0;
            for (int i = 0; i < tab.length; ++i)
                tab[i] = null;
        }
    }
  • 删除节点的步骤:

    1. 首先判断集合中是否存在该key的hashcode的节点位置,有则继续,没有则没有匹配项
    2. 如果有位桶数组存在该索引,则对首节点和后继节点的判断逻辑分开处理,这是因为首节点没有数据结构相关性。而后继节点需要判断是红黑树结构还是链表结构。总之在该阶段就是遍历节点,找到要删除的节点,交给第三步
    3. 获取到要删除的节点,首先根据传入方法的参数确定是否需要匹配value。其次要判断该节点是属于红黑树节点还是链表节点,分别执行不同的删除节点方法
  • clear()方法仅仅是清空数组的所有元素,每个位置都指向null不会改变数组的大小和hashMap的临界值等


15 - replace函数
	/**
     * 公共方法:将键值为key的oldValue用newValue替换
     * @param key
     * @param oldValue
     * @param newValue
     * @return
     */
    @Override
    public boolean replace(K key, V oldValue, V newValue) {
        java.util.HashMap.Node<K, V> e;
        V v;
        //在hashMap中找到key对应的的节点,如果该节点不等于null
        //且 (该节点的value等于参数oldValue 或者 该节点的value不等于null且与oldValue匹配)
        if ((e = getNode(hash(key), key)) != null &&
                ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
            //我们则把该节点value赋值为newValue
            e.value = newValue;
            afterNodeAccess(e);
            //赋值成功,返回true
            return true;
        }
        //未找到对应节点或节点的value与oldValue不匹配则返回false
        return false;
    }

    /**
     * 公共方法:将键值为key的value用参数value替换
     * @param key
     * @param value
     * @return
     */
    @Override
    public V replace(K key, V value) {
        java.util.HashMap.Node<K, V> e;
        //如果hashMap中有该key的对应节点
        if ((e = getNode(hash(key), key)) != null) {
            V oldValue = e.value;
            //则新值替换旧值
            e.value = value;
            afterNodeAccess(e);
            //返回旧值
            return oldValue;
        }
        //如果没有匹配节点,则返回null
        return null;
    }
  • 替换方法有两个重载方法。一个是只传key和value就可以,这一种不去匹配value是否相等。另一个方法则要传入旧值,可以用于另一种用于,只有value是我预期的value,我才发生替换

16 - compute,computeIfAbsent,computeIfPresent函数
 	/**
     * 公有方法
     * 作用就是:取出该key的节点,对该节点key和value当参数传入lambda方法
     * 经过lambda方法处理后得出处理后的value,并存回map(更新旧值)
     * 简单的理解就是,取出对应的value,对其处理后,存回去,处理过程用lambda表达式实现
     * 其他过程这个方法会帮你做,你只有提供key和lambda形式的处理函数
     *
     * @param key
     * @param remappingFunction 二原函数接口
     * @return
     */
    @Override
    public V compute(K key,
                     BiFunction<? super K, ? super V, ? extends V> remappingFunction) {

        if (remappingFunction == null)
            throw new NullPointerException();
        int hash = hash(key);
        java.util.HashMap.Node<K, V>[] tab;
        java.util.HashMap.Node<K, V> first;
        int n, i;
        int binCount = 0;
        java.util.HashMap.TreeNode<K, V> t = null;
        java.util.HashMap.Node<K, V> old = null;
        if (size > threshold || (tab = table) == null ||
                (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((first = tab[i = (n - 1) & hash]) != null) {
            if (first instanceof java.util.HashMap.TreeNode)
                old = (t = (java.util.HashMap.TreeNode<K, V>) first).getTreeNode(hash, key);
            else {
                java.util.HashMap.Node<K, V> e = first;
                K k;
                do {
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k)))) {
                        old = e;
                        break;
                    }
                    ++binCount;
                } while ((e = e.next) != null);
            }
        }
        V oldValue = (old == null) ? null : old.value;
        V v = remappingFunction.apply(key, oldValue);
        if (old != null) {
            if (v != null) {
                old.value = v;
                afterNodeAccess(old);
            } else
                removeNode(hash, key, null, false, true);
        } else if (v != null) {
            if (t != null)
                t.putTreeVal(this, tab, hash, key, v);
            else {
                tab[i] = newNode(hash, key, v, first);
                if (binCount >= TREEIFY_THRESHOLD - 1)
                    treeifyBin(tab, hash);
            }
            ++modCount;
            ++size;
            afterNodeInsertion(true);
        }
        return v;
    }


	/**
     * compute的不存在则处理版本
     *
     * @param key
     * @param mappingFunction
     * @return
     */
    @Override
    public V computeIfAbsent(K key,
                             Function<? super K, ? extends V> mappingFunction) {
        if (mappingFunction == null)
            throw new NullPointerException();
        int hash = hash(key);
        java.util.HashMap.Node<K, V>[] tab;
        java.util.HashMap.Node<K, V> first;
        int n, i;
        int binCount = 0;
        java.util.HashMap.TreeNode<K, V> t = null;
        java.util.HashMap.Node<K, V> old = null;
        if (size > threshold || (tab = table) == null ||
                (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((first = tab[i = (n - 1) & hash]) != null) {
            if (first instanceof java.util.HashMap.TreeNode)
                old = (t = (java.util.HashMap.TreeNode<K, V>) first).getTreeNode(hash, key);
            else {
                java.util.HashMap.Node<K, V> e = first;
                K k;
                do {
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k)))) {
                        old = e;
                        break;
                    }
                    ++binCount;
                } while ((e = e.next) != null);
            }
            V oldValue;
            if (old != null && (oldValue = old.value) != null) {
                afterNodeAccess(old);
                return oldValue;
            }
        }
        V v = mappingFunction.apply(key);
        if (v == null) {
            return null;
        } else if (old != null) {
            old.value = v;
            afterNodeAccess(old);
            return v;
        } else if (t != null)
            t.putTreeVal(this, tab, hash, key, v);
        else {
            tab[i] = newNode(hash, key, v, first);
            if (binCount >= TREEIFY_THRESHOLD - 1)
                treeifyBin(tab, hash);
        }
        ++modCount;
        ++size;
        afterNodeInsertion(true);
        return v;
    }

    /**
     * compute的存在才处理版本
     *
     * @param key
     * @param remappingFunction
     * @return
     */
    public V computeIfPresent(K key,
                              BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        if (remappingFunction == null)
            throw new NullPointerException();
        java.util.HashMap.Node<K, V> e;
        V oldValue;
        int hash = hash(key);
        if ((e = getNode(hash, key)) != null &&
                (oldValue = e.value) != null) {
            V v = remappingFunction.apply(key, oldValue);
            if (v != null) {
                e.value = v;
                afterNodeAccess(e);
                return v;
            } else
                removeNode(hash, key, null, false, true);
        }
        return null;
    }

   
  • 目前不对里面的代码进行解析,不深究,会用就行
  • 三个方法如果在lambda表达式中处理后的value为Null则代表删除键值为Key的这个节点,而不是将值更新为null
  • compute是其他两个方法的结合体,如果不存在Key的情况下,依然会插入
  • Java8 Map的compute()方法 - @作者:Peng
  • 例子: map.compute(“key” , (key, value) -> key + value )

17 - merge函数
    /**
     * 公共方法
     * merge方法跟compute类似,都是取Key的节点,通过lambda计算过程得出新value, 插回map
     * 但区别就在lambda表达式中,三个泛型都是与V有关的。实际传入key和newValue,且lambda传入的两个参数,一个是oldValue,一个是newValue
     * 而compute,除了lambda表达式,只有一个参数key,且lambda中一个参数是key,另一参数是旧value
     * 
     * 所以我们可以知道merge的目的就是想新值与旧值做计算后成为一个新值,再插回去
     *
     * @param key
     * @param value
     * @param remappingFunction
     * @return
     */
    @Override
    public V merge(K key, V value,
                   BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        if (value == null)
            throw new NullPointerException();
        if (remappingFunction == null)
            throw new NullPointerException();
        int hash = hash(key);
        java.util.HashMap.Node<K, V>[] tab;
        java.util.HashMap.Node<K, V> first;
        int n, i;
        int binCount = 0;
        java.util.HashMap.TreeNode<K, V> t = null;
        java.util.HashMap.Node<K, V> old = null;
        if (size > threshold || (tab = table) == null ||
                (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((first = tab[i = (n - 1) & hash]) != null) {
            if (first instanceof java.util.HashMap.TreeNode)
                old = (t = (java.util.HashMap.TreeNode<K, V>) first).getTreeNode(hash, key);
            else {
                java.util.HashMap.Node<K, V> e = first;
                K k;
                do {
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k)))) {
                        old = e;
                        break;
                    }
                    ++binCount;
                } while ((e = e.next) != null);
            }
        }
        if (old != null) {
            V v;
            if (old.value != null)
                v = remappingFunction.apply(old.value, value);
            else
                v = value;
            if (v != null) {
                old.value = v;
                afterNodeAccess(old);
            } else
                removeNode(hash, key, null, false, true);
            return v;
        }
        if (value != null) {
            if (t != null)
                t.putTreeVal(this, tab, hash, key, value);
            else {
                tab[i] = newNode(hash, key, value, first);
                if (binCount >= TREEIFY_THRESHOLD - 1)
                    treeifyBin(tab, hash);
            }
            ++modCount;
            ++size;
            afterNodeInsertion(true);
        }
        return value;
    }
  • 如果在lambda表达式中处理后的value为Null则代表删除键值为Key的这个节点,而不是将值更新为null
  • **例子:**map.merge(“key”, " newValue", (oldValue, newValue) -> oldValue + newValue)

18 - values,keySet,entrySet函数
	/**
     * 返回HashMap的Key集合
     * 
     * @return
     */
	public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new java.util.HashMap.KeySet();
            keySet = ks;
        }
        return ks;
    }

	/**
     * 返回HashMap的value集合
     * 
     * @return
     */
    public Collection<V> values() {
        //values是在AbstractMap中定义的成员变量
        Collection<V> vs = values;
        if (vs == null) {
            vs = new java.util.HashMap.Values();
            values = vs;
        }
        return vs;
    }
    
 	/**
     * 返回存储有key和value的Set集合
     * 
     * @return Set<Map.Entry<K, V>>
     */
	public Set<Map.Entry<K, V>> entrySet() {
        Set<Map.Entry<K, V>> es;
        return (es = entrySet) == null ? (entrySet = new java.util.HashMap.EntrySet()) : es;
    }
  • 只看HashMap的这些代码看不出什么,KeySet和Values的具体实现在AbstractMap中完成的
  • KeySet和Values和EntrySet都是HashMap中的内部类
  • EntrySet就是一个泛型为Node<k,v>类型的节点集合

19 - forEach函数
	/**
     * foreach的lambda函数式版本
     * 
     * @param action
     */
    @Override
    public void forEach(BiConsumer<? super K, ? super V> action) {
        java.util.HashMap.Node<K, V>[] tab;
        //consumer不为空则继续
        if (action == null)
            throw new NullPointerException();
        //如果map有数据,且数组已经初始化
        if (size > 0 && (tab = table) != null) {
            //获得版本
            int mc = modCount;
            //遍历位桶数组
            for (int i = 0; i < tab.length; ++i) {
                //遍历链表或红黑树
                for (java.util.HashMap.Node<K, V> e = tab[i]; e != null; e = e.next)
                    //所有节点都指向lambda的action方法,对每个节点的key和value都进行消费
                    action.accept(e.key, e.value);
            }
            //避免线程安全问题,如果在遍历过程中发现目前版本不是遍历时刻记录的版本,则抛异常
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
  • foreach的函数式编程版本

20 - replaceAll函数
	/**
     * 根据lambda函数的处理替换整个map的value
     * 就是遍历整个集合,每个原的key和value作为lambda函数的参数,计算得出新value,新值更新旧值
     * 即替换Map中所有元素的value值,这个值由旧的key和value计算得出,接收参数 (K, V) -> V
     * 
     * @param function
     */
    @Override
    public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
        java.util.HashMap.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) {
                for (java.util.HashMap.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();
        }
    }
  • 具体就不分析了,重点仅仅是BiFunction接口
  • 例子: map.replaceAll(key,value -> key + value)

21 - clone函数
	/**
     * 原型模式-clone方法
     * 
     * @return
     */
    @SuppressWarnings("unchecked")
    @Override
    public Object clone() {
        java.util.HashMap<K, V> result;
        try {
            //调用父类AbstractMap的克隆方法,实际AbstractMap用的是Object的clone()
            result = (java.util.HashMap<K, V>) super.clone();
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
        //将所有的size,threshold等属性恢复成初始默认值
        result.reinitialize();
        //通过putMapEntries方法,将当前集合的所有元素塞进新map(result)中,并返回
        result.putMapEntries(this, false);
        return result;
    }
	
	/**
	* 初始化hashmap的字段属性
	*
	*/
	void reinitialize() {
        table = null;
        entrySet = null;
        keySet = null;
        values = null;
        modCount = 0;
        threshold = 0;
        size = 0;
    }

22 - loadFactor函数
	final float loadFactor() {
        return loadFactor;
    }
  • 默认不可继承方法,返回负载因子

23 - capacity函数
	/**
     * 默认不可继承方法:返回当前集合可承受的容量,即当前不扩容情况下最多可容量的元素个数
     * 
     * @return
     */
    final int capacity() {
        //如果数组不为空,返回数组长度
        //如果为空,则看临界值是否大于0,如果大于0,则返回当前临界值,否则返回初始临界值
        return (table != null) ? table.length :
                (threshold > 0) ? threshold :
                        DEFAULT_INITIAL_CAPACITY;
    }

24 - writeObject,readObject函数
    private void readObject(java.io.ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        // Read in the threshold (ignored), loadfactor, and any hidden stuff
        s.defaultReadObject();
        reinitialize();
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new InvalidObjectException("Illegal load factor: " +
                    loadFactor);
        s.readInt();                // Read and ignore number of buckets
        int mappings = s.readInt(); // Read number of mappings (size)
        if (mappings < 0)
            throw new InvalidObjectException("Illegal mappings count: " +
                    mappings);
        else if (mappings > 0) { // (if zero, use defaults)
            // Size the table using given load factor only if within
            // range of 0.25...4.0
            float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
            float fc = (float) mappings / lf + 1.0f;
            int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                    DEFAULT_INITIAL_CAPACITY :
                    (fc >= MAXIMUM_CAPACITY) ?
                            MAXIMUM_CAPACITY :
                            tableSizeFor((int) fc));
            float ft = (float) cap * lf;
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                    (int) ft : Integer.MAX_VALUE);

            // Check Map.Entry[].class since it's the nearest public type to
            // what we're actually creating.
            SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
            @SuppressWarnings({"rawtypes", "unchecked"})
            java.util.HashMap.Node<K, V>[] tab = (java.util.HashMap.Node<K, V>[]) new java.util.HashMap.Node[cap];
            table = tab;

            // Read the keys and values, and put the mappings in the HashMap
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }
        }
    }

  • 序列化的时候用的,用的比较少,所以暂时就不研究了
  • 都是私有方法,是内部使用的

25 - internalWriteEntries函数
 	// Called only from writeObject, to ensure compatible ordering.
    void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
        java.util.HashMap.Node<K, V>[] tab;
        if (size > 0 && (tab = table) != null) {
            for (int i = 0; i < tab.length; ++i) {
                for (java.util.HashMap.Node<K, V> e = tab[i]; e != null; e = e.next) {
                    s.writeObject(e.key);
                    s.writeObject(e.value);
                }
            }
        }
    }
  • 注释也说的很明白,仅仅是writeObject来调用来的,确保

26 - newNode,newTreeNode,replacementNode,replacementTreeNode函数

	// Create a regular (non-tree) node,创建普通节点
    java.util.HashMap.Node<K, V> newNode(int hash, K key, V value, java.util.HashMap.Node<K, V> next) {
        return new java.util.HashMap.Node<>(hash, key, value, next);
    }

    // For conversion from TreeNodes to plain nodes,将树形节点转换成普通节点
    java.util.HashMap.Node<K, V> replacementNode(java.util.HashMap.Node<K, V> p, java.util.HashMap.Node<K, V> next) {
        return new java.util.HashMap.Node<>(p.hash, p.key, p.value, next);
    }

    // Create a tree bin node,创建树形节点
    java.util.HashMap.TreeNode<K, V> newTreeNode(int hash, K key, V value, java.util.HashMap.Node<K, V> next) {
        return new java.util.HashMap.TreeNode<>(hash, key, value, next);
    }

    // For treeifyBin,将普通节点转换为树形节点
    java.util.HashMap.TreeNode<K, V> replacementTreeNode(java.util.HashMap.Node<K, V> p, java.util.HashMap.Node<K, V> next) {
        return new java.util.HashMap.TreeNode<>(p.hash, p.key, p.value, next);
    }
  • 新建链表节点和红黑树节点的方法,以及两种节点项目转换的方法

27 - afterNodeAccess,afterNodeInsertion,afterNodeRemoval函数
	// Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(java.util.HashMap.Node<K, V> p) {
    }

    void afterNodeInsertion(boolean evict) {
    }

    void afterNodeRemoval(java.util.HashMap.Node<K, V> p) {
    }

### 相关问题 ----
容量,临界值,当前大小区别
  • table.length,数组的长度,就是HashMap的容量,既常说的capacity
  • threshold 就是临界值,HashMap不扩容情况下,实际能存储的大小值,等于capacity * loadfactor
  • size 就是当前HashMap实际存储的元素个数,既数组中存在元素的索引个数
  • 因为capacity容量是没有成员对象来标识的,所以初始阶段map的容量由threshold来暂时代替,当进入初始化阶段时,才会通过threshold来分配数组的.length, 有了length,才能重新分配threshold

为什么要对HashMap进行容量控制?
  • 为了避免出现空间浪费,如果我们一开始的默认值比较大,则会比较浪费,所以实现了动态扩容
  • 另外如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组数组很小,即使好的Hash算法也会出现较多碰撞,所以就需要在空间成本和时间成本之间权衡,所以我们就需要根据实际情况确定哈希桶数组的大小,并在此基础上设计好的hash算法减少Hash碰撞

为什么HashMap哈希函数的扰动函数?
	static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

也许你是非常奇怪,为什么HashMap的hash函数需要将Key的原哈希码在与原哈希码右移16位后的结果相与运算?

  • 其实这个一个扰动函数,作用就是为了降低哈希冲突的概率。
  • 一般自带的类型的key的哈希值都是Int整型的范围,既-2147483648(-2^31)2147483647(2^31 - 1) , 且我们看了HashMap的源码之后,也会知道,HashMap是根据key的哈希值 mod底层table长度来得到数据存放位置(索引)的。
  • 但因为一般来说key的原哈希值会非常的大,远大于底层数据table的长度。所以当原哈希值 & len - 1 , 就会造成len - 1位之外的高位丢失,只剩下低位相与。所以如果有两个key的哈希值的高位完全不同,相差很大,而低位基本相同,就会造成哈希值原本相差很大的元素被存放在table数组的同一个位置,出现哈希冲突,如下图5749693,8388605两个相差很大的哈希值,在于15相与后,最终却得到一样的值,代表他们存放在table数组的索引为15的位置
    在这里插入图片描述
  • 所以我们知道了,如果我们直接拿key的原哈希值来求索引位置,会存在很大的缺陷,哈希冲突问题严重。所以HashMap就想了一个办法,对原哈希值的低位进行"扰动", 增加一些不可确定的因素,提高哈希函数的容错性。这里就采用原哈希值,与原哈希值右移16位的结果相与(hash & (hash >> 16))
    在这里插入图片描述
  • 以上图举例,我们同样是这两个5749693,8388605哈希值,采用了扰动函数之后,我们最终取索引运算就可以得到不同的索引位,其对应的元素就回存放在table数组的不同位置上,并没有产生哈希冲突!!

在这里插入图片描述
图片源于网络

我们知道扰动函数具有降低哈希冲突的效果,那么它的原理是什么呢?

  • 我们知道哈希值一般的取值范围就是int取值范围,既-2147483648(-2^31)2147483647(2^31 - 1),而将原哈希值右移16位(hash >>16),就相当于把32位的int值,右移了一半的位置。说白了,右移的结果就是高位16位,既高位16位 = hash >> 16

  • hash & (hash >> 16) 说白了就是将哈希值的高位16位和低位16位进行中和运算,得到一个新的哈希值。目的很明显,就是以前的去索引运算,只对低位进行运算,高位即使完全不同,也是无法产生任何影响的。而有了扰动函数,那么高位会影响到低位的值的变化,所以就可以变相让高位也参与mod运算,降低哈希冲突

  • 为什么右移16呢? 真的是以为刚好位移int的一半长度吗?也是有关系的。但是更大的关系是因为Interger的整数最大值是2^16 - 1 , 所以HashMap底层table数组的最大长度也超不过int的16位。所以取模运算永远最多只有低位16位会参与运算。所以扰动函数才刚好将高位16位于低位16位进行中和

  • JDK 源码中 HashMap 的 hash 方法原理是什么?- @知乎


为什么HashMap的哈希桶数组Table的长度length大小必须是2的n次方?
  • 在HashMap中,哈希桶数组table的长度length大小必须为2的n次方(一定是合数),这是一种非常规的设计,常规的设计是把桶的大小设计为素数。相对来说素数导致冲突的概率要小于合数。HashMap采用这种非常规设计,主要是为了在取模和扩容时做优化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。

  • 说白了,就是为了对一些运算进行优化,比如取模动作,我们通常是通过hash % len的长度来确定该对象在数组的哪一个索引位置存放;如果len是2的n次方的情况下,我们可以用位运算hash & len - 1来代替取模运算;位运算的效果更高。

  • 总之,容量为2幂次方是很多二进制优化的前提条件,比如取索引运算要基于该条件作为前提,resize


Java 8 为什么引入红黑树结构?
  • 即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能

table数组和内部类EntrySet的关系?
  • EntrySet实际是一个视图数据结构,并不存放实际的数据。而table数组,准确的说是Node数组才是存放HashMap具体的数据的。同理还有KeySet, Values等内部类
  • 既EntrySet的对外操作,实际调用的还是node数组,其本身没有存储结构
	public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }
  • 我们可以在putV以上是HashMap返回

重点理解Put()元素

PutVal()存在的挑战

  • 链表长度超过8,就要转换为红黑树,小于8,又要退化成链表
  • 判断是否存在重复key , 有则直接覆盖,达到去重效果

PutVal函数的步骤

  • 判断是否是空集合,如果是初始化数组长度
  • 不为空,则获取新元素要插入的位置,即在table中的位置
  • 如果该位置上没有其他元素,则直接把新元素放在该位置上
  • 如果该位置上已经有其他元素了,则新元素跟该位置上的元素进行比较,是否是同一个元素, 既key是否相同
  • 如果是直接更新value,如果不是则判断该位置的数据结构是链表还是红黑树
  • 如果是红黑树则执行红黑树的putTreeVal方法,如果是链表则循环链表找到尾节点
  • 遍历链表过程中,如果某节点的下一个节点是null, 代表其实尾节点,就直接在next上存放新元素,插完节点后还要判断链表长度是否大于等于8,看是否要将链表转换成红黑树,最后打破循环,最后return null
  • 遍历在寻找尾节点过程中,发现有相同key, 而打破for循环,会执行新值替换旧值方法,最终返回旧值

在这里插入图片描述

  • 如果是尾部插入新元素,需要增加版本号和判断是否扩容;如果是新值替换旧值是不需要增加版本号和判断扩容的。既底层结构框架发生变化时才需要修改版本号
  • 是否走版本号+1,和扩容判断是依据临时变量e来判断的,e == null时,就会走版本号扩容路线
  • 扩容判断是一句,当前size+1后的size与临界值比较,如果size大于临界值,就需要进行扩容,执行resize方法

重点理解Resize()扩容机制

Table数组的初始化时机

  • HashMap的内存分配属于懒加载的机制,既你通过构造函数,只要不插入元素的情况下,它并不会为底层table数组分配任何空间。只有当你第一次插入元素的时候,它才会触发扩容机制,分配初始空间大小。

ArrayList和HashMap的不同

  • ArrayList和HashMap都有一开始传入容量大小的构造函数,但是HashMap属于懒加载模式,而ArrayList则输入立即加载模式。既传入了一个大小,HashMap只是把赋值给threshold成员变量,而什么都没做。但ArrayList就会直接new一个传入大小的数组,直接分配好内存
  • 当然他们也有相同之处,那就是ArrayList和HashMap的无参构造,都是懒加载模式。既ArrayList第一次插入元素,才会触发0->10的扩容。HashMap第一次插入元素,也才会触发table数组的初始化

Resize()扩容函数的两个作用

  • 一是初始化table数组,因为hashmap是懒加载机制
  • 二是容量不够,对map进行扩容,重新生产table数组。通常是判断当前map是否大于临界值

HashMap的初始扩容

  • 无参构造:初次插入一个元素,第一次扩容,分配16大小的数组空间
  • 指定大小的构造:初次插入一个元素,第一次扩容,分配指定大小的数组空间
  • 无参构造:初次插入一批的元素,会在判断插入这一批元素需要多大的容量,然后一次扩容出能容纳这么大的空间,最后一个一个的putVal
  • 指定大小的构造:初次插入一批元素,跟上面一样,会判断当前容量是否满足插入这么元素,如果不能,一次扩容到能容纳这么多。并不会扩容很多次

Resize函数存在的挑战

  • rehash的问题

扩容的步骤

  • 计算新容量阶段,计算数组扩容后的新容量和新临界值
  • 重新生成数组阶段,根据新容量初始化新数组,即新位桶
  • ReHash阶段,将旧数组中的元素取出,拷贝至新数组

Java7,8的主要区别

  • Java8增加了红黑树结构
  • resize扩容函数修改了原Java7的rehash方法
  • 7的rehash会将冲突元素的链表倒置,因为7的rehash过程,是单链表的头插入法,从原链头开始rehash,完后从链头插入。而8的原理这不是重新计算hash
  • 8的rehash阶段实际并没有rehash,而是通过一种非常巧妙的方式实现类似rehash的效果

8的rehash的分析

		java.util.HashMap.Node<K, V> loHead = null, loTail = null;
        java.util.HashMap.Node<K, V> hiHead = null, hiTail = null;
        java.util.HashMap.Node<K, V> next;
        do {
            //next是首节点的直接后继节点
            next = e.next;
            //记住旧容量的二进制形式的唯一的1对应的位置,然后求新增元素hash码对应位置的值是否是0
            //如果是0,那么元素在新数组中的索引不变
            if ((e.hash & oldCap) == 0) {
                if (loTail == null)
                    loHead = e;
                else
                    loTail.next = e;
                loTail = e;
                //记住旧容量的二进制形式的唯一的1对应的位置,然后求新增元素hash码对应位置的值是否是·
                //如果是1,则新索引是原索引+oldCap(原数组长度)
            } else {
                if (hiTail == null)
                    hiHead = e;
                else
                    hiTail.next = e;
                hiTail = e;
            }
        } while ((e = next) != null);
        // 原索引放到bucket里
        if (loTail != null) {
            loTail.next = null;
            newTab[j] = loHead;
        }
        // 原索引+oldCap放到bucket里
        if (hiTail != null) {
            hiTail.next = null;
            newTab[j + oldCap] = hiHead;
        }
  • 它的原理就不是重新计算hash值,而是利用二进制的巧妙计算实现类似rehash的效果。既当前元素的hash值 & oldcap == 0, 就代表当前元素在新数组的索引不变,如果大于0,就说明当前元素在新元素的索引等于旧索引 + oldcap(旧数组长度)

  • 为什么可以这样呢?原理是什么呢?原理我无法证明,但我可以简单的拿数据告诉你。这样的巧妙实现的基本依赖之一,就是我们的map大小永远都是2的幂次方大小

  • 52 & 15 == 4 , 52 & 31 == 20 如果map长度为16,hash值为52的值在原索引的位置是4,map扩容为32后。52 & 16 == 16 > 0,所以新索引 = 旧索引 + 16 = 20, 我们验证一下看看,52 & 31 的确是 20
    在这里插入图片描述

  • 总之呢,你可以复杂的说hash二进制形式对应bit为1,或者简单的说hash & oldcap > 1都可以,总之因为容量是2的次幂的特性,rehash可以有以上的规律进行优化


参考资料


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值