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位,所以是16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16//最大容量,向左移30位,1073741824,Integer最大为1073741824static 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;//计数器,版本号,每次修改就会+1transient 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类型,返回creturn 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() == //且该泛型接口的原生类型是ComparableComparable.class) &&                //且...就返回c,c就是x的类类型,即Class类型(as = p.getActualTypeArguments()) != null &&as.length == 1 && as[0] == c) // type arg is creturn c;}}}return null;}@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparablestatic 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;  //获取临时tableif ((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;                              //初始化新容量和新临界值的临时变量/*** 这个阶段在计算获取数组新容量和新临界值*///如果旧数组的长度大于0if (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 thresholdnewCap = oldThr;//如果旧数组长度小于等于0,且旧临界值也小于等于0,则初始化容量和临界值else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;  //默认16newThr = (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 orderjava.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;elseloTail.next = e;loTail = e;//如果扩容后,容量二进制结构中新增的那一位对应旧索引的位置的值是1//则新索引是原索引+oldCap} else {if (hiTail == null)hiHead = e;elsehiTail.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,则返回空,如果不为空则返回节点.valuereturn (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*/@Overridepublic 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上存放新节点元素,newNodeif ((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 keyV oldValue = e.value;  //获取旧节点的值if (!onlyIfAbsent || oldValue == null) //如果onlyIfAbsent为false或oldV为nulle.value = value; //用新值替换旧值afterNodeAccess(e);return oldValue; //返回旧值}}//每次修改集合,版本号+1,更新旧值不会触发下面操作,也不会更改版本号++modCount;            //如果集合容量大于临界值,则扩容if (++size > threshold) resize();           //扩容afterNodeInsertion(evict);return null;}/*** put()的不覆盖旧值版本* @param key* @param value* @return*/@Overridepublic 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) {//如果数据结构是红黑树结构,则通过红黑树方法获取对应节点位置,赋值给nodeif (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)))) {//如果是则找到对应节点,赋值给nodenode = 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在第二步时,被用于记录删掉节点的前一节点地址)elsep.next = node.next;//每次发生修改,版本号+1++modCount;//hashmap大小-1--size;//删除节点后续操作afterNodeRemoval(node);//返回删除节点return node;}}//没有匹配节点时,不删除,返回nullreturn null;}/*** 公有方法:清空hashMap所有元素* 不过这种方式仅仅是让数组清空数据。* 数组的长度,临界值等依旧*/public void clear() {java.util.HashMap.Node<K, V>[] tab;//清空hashMap必然发生改变,所以版本+1modCount++;//如果位桶数组不等于null,且有数据if ((tab = table) != null && size > 0) {//则size = 0,每个索引所存放的对象都改为nullsize = 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*/@Overridepublic 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赋值为newValuee.value = newValue;afterNodeAccess(e);//赋值成功,返回truereturn true;}//未找到对应节点或节点的value与oldValue不匹配则返回falsereturn false;}/*** 公共方法:将键值为key的value用参数value替换* @param key* @param value* @return*/@Overridepublic 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;}//如果没有匹配节点,则返回nullreturn 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*/@Overridepublic 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);} elseremoveNode(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*/@Overridepublic 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;} elseremoveNode(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*/@Overridepublic 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);elsev = value;if (v != null) {old.value = v;afterNodeAccess(old);} elseremoveNode(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*/@Overridepublic 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*/@Overridepublic 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")@Overridepublic 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 Cloneablethrow 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 stuffs.defaultReadObject();reinitialize();if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new InvalidObjectException("Illegal load factor: " +loadFactor);s.readInt();                // Read and ignore number of bucketsint 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.0float 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 HashMapfor (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-actionsvoid 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;elseloTail.next = e;loTail = e;//记住旧容量的二进制形式的唯一的1对应的位置,然后求新增元素hash码对应位置的值是否是·//如果是1,则新索引是原索引+oldCap(原数组长度)} else {if (hiTail == null)hiHead = e;elsehiTail.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可以有以上的规律进行优化

参考资料


  • Java8 HashMap源码解析 - @作者:qazwyc
  • Java源码分析:HashMap 1.8 相对于1.7 到底更新了什么? - @作者:Carson_Ho
  • Java 8系列之重新认识HashMap - @作者: 美团技术团队

【Java源码分析】Java8的HashMap源码分析相关推荐

  1. 【Java深入研究】9、HashMap源码解析(jdk 1.8)

    一.HashMap概述 HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现.与HashTable主要区别为不支持同步和允许null作为key和value.由于HashMap不是线程 ...

  2. 【Java自顶向下】面试官:HashMap源码看过吗?我:看过!面试官:好极了,那么来扒一扒吧!

    HashMap 关于hash表的基础内容,请看文章 [数据结构-查找]3.散列表详解 [Java自顶向下]HashMap面试题(2021最新版) 顶层应用 public class HashMapTe ...

  3. hashmap remove 没释放内存_java从零开始手写 redis(13)HashMap 源码原理详解

    为什么学习 HashMap 源码? 作为一名 java 开发,基本上最常用的数据结构就是 HashMap 和 List,jdk 的 HashMap 设计还是非常值得深入学习的. 无论是在面试还是工作中 ...

  4. Java8 HashMap源码分析

    前言 今天,我们主要来研究一下在Java8中HashMap的数据结构及一些重要方法的具体实现.       研究HashMap的源代码之前,我们首先来研究一下常用的三种数据结构:数组.链表和红黑树. ...

  5. Java源码详解二:HashMap源码分析--openjdk java 11源码

    文章目录 HashMap.java介绍 1.HashMap的get和put操作平均时间复杂度和最坏时间复杂度 2.为什么链表长度超过8才转换为红黑树 3.红黑树中的节点如何排序 本系列是Java详解, ...

  6. 【Java源码分析】Java8的ArrayList源码分析

    Java8的ArrayList源码分析 源码分析 ArrayList类的定义 字段属性 构造函数 trimToSize()函数 Capacity容量相关的函数,比如扩容 List大小和是否为空 con ...

  7. [Java] HashMap 源码简要分析

    特性 * 允许null作为key/value. * 不保证按照插入的顺序输出.使用hash构造的映射一般来讲是无序的. * 非线程安全. * 内部原理与Hashtable类似. 源码简要分析 publ ...

  8. Java类集框架 —— HashMap源码分析

    HashMap是基于Map的键值对映射表,底层是通过数组.链表.红黑树(JDK1.8加入)来实现的. HashMap结构 HashMap中存储元素,是将key和value封装成了一个Node,先以一个 ...

  9. java基础之HashMap源码分析

    目录 1. HashMap原理分析 1.1. HashMap继承体系 1.2.Node数据结构分析 1.3.底层储存结构 1.3.1.put方法分析 1.4.hash碰撞 1.4.1.key值的唯一性 ...

最新文章

  1. C++ Opengl纹理贴图源码
  2. 二十四、数据挖掘时序模式
  3. struts2几种result type探究
  4. 我的世界java版和基岩版对比_我的世界:基岩版比Java多出的七个特性,都听过的非老mc莫属了!...
  5. 970页绝版资料!初高中数学与竞赛知识点+方法技巧,由苏步青当顾问,众多一线名师共同编写!...
  6. 请对比html与css的异同,css3与css2的区别是什么?
  7. Java实现连连看源代码文档_Java实现游戏连连看(有源代码)
  8. swift建立桥接_在Swift中建立Alexa技能
  9. python中redirect_详解如何用django实现redirect的几种方法总结
  10. linux7配网卡,CentOS 7 配置网卡
  11. STM32F103单片机JTAG端口重映射
  12. JS 获取元素当前的样式信息
  13. 算法笔记_面试题_18.动态规划_模板及示例十几道(上)
  14. Java GC种类以及触发时机
  15. hasp运行不成功_HASP加密狗驱动程序没有安装成功如何解决
  16. C++_SHFileOperation文件夹操作
  17. 让你的工作变轻松的一套免费的 iPhone 手势图标
  18. 转 解决打印机无法打印的方法
  19. Spark中广播变量(boardcast)的使用
  20. 2022-2028年全球及中国红外(IR)传感器行业投资前景分析

热门文章

  1. Flash制作简单塔防游戏(一)
  2. js浮动运动函数html,JS+CSS动态绘制元素曲线运动轨迹(数学函数)
  3. 单引号和双引号的区别(字符串字面量 字符常量) | C语言
  4. UG476-Xilinx-7Series-FPGA高速收发器使用学习—RX接收端介绍
  5. android 自动生成id,《转载》生成Android设备的唯一ID
  6. 微众银行2021前端实习生笔试
  7. java输出数组中的元素_java一行代码输出数组的所有元素内容
  8. 2022年 change detection遥感图像变化检测 论文附代码
  9. 智能硬件的时代划分:如何向外行装逼
  10. QQ浏览器极速内核关闭“您即将提交的信息不安全”提示