【Java源码分析】Java8的HashMap源码分析
Java8中的HashMap源码分析
源码分析
- HashMap的定义
- 字段属性
- 构造函数
- hash函数
- comparableClassFor,compareComparables函数
- tableSizeFor函数
- putMapEntries,putAll函数
- size,isEmpty函数
- containsKey,containsValue函数
- resize函数(扩容机制)
- treeifyBin函数
- get,getNode,getOrDefault函数
- put,putVal,putIfAbsent函数
- remove,removeNode,clear函数
- replace函数
- compute,computeIfAbsent,computeIfPresent函数
- values,keySet,entrySet函数
- merge函数
- forEach函数
- replaceAll函数
- clone函数
- loadFactor函数
- capacity函数
- writeObject,readObject函数
- internalWriteEntries函数
- newNode,newTreeNode,replacementNode,replacementTreeNode函数
- afterNodeAccess,afterNodeInsertion,afterNodeRemoval函数
内部类分析
- KeySet
- Values
- EntrySet
- HashIterator
- KeyIterator
- ValueIterator
- EntryIterator
- Node<K,V>
- HashMapSpliterator<K,V>
- KeySpliterator<K,V>
- ValueSpliterator<K,V>
- EntrySpliterator<K,V>
- 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个大模块:
- 计算数组扩容后的新容量和新临界值
- 根据新容量初始化新数组,即新位桶
- 将旧数组中的元素取出,拷贝至新数组
- 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的步骤是:
- 首先根据hash确定在table的位置(即hash桶的位置)
- 判断首节点是否要找的key
- 判断链表是否已转换成红黑树结构,如果是则调用红黑树查询函数,如果不是则继续
- 如果不是红黑树结构则遍历链表寻找对应Key的Node
(first = tab[(n - 1) & hash])
实际是first = tab[hash % n]
的优化 模运算的优化 - @作者:frapplesif (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的步骤:
- 判断是否是空集合,如果是初始化数组长度
- 不为空,则获取新元素要插入的位置,即在
table
中的位置 - 如果该位置上没有其他元素,则直接把新元素放在该位置上
- 如果该位置上已经有其他元素了,则新元素跟该位置上的元素进行比较,是否是同一个元素
- 如果是直接更新
value
,如果不是则判断该位置的数据结构是链表还是红黑树 - 如果是红黑树则执行红黑树的
putTreeVal
方法,如果是链表则循环链表找到尾节点 - 如果尾节点的下一个节点是
null
,就直接在next上存放新元素,插完节点后还要判断链表长度是否大于等于8,看是否要将链表转换成红黑树,最后打破循环,最后returnnull
- 在找尾节点的过程中,如果其中一个节点跟新元素是同一元素,即key相等,则打破循环
- 在寻找尾节点过程中,发现有相同元素而打破
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;}}
删除节点的步骤:
- 首先判断集合中是否存在该key的hashcode的节点位置,有则继续,没有则没有匹配项
- 如果有位桶数组存在该索引,则对首节点和后继节点的判断逻辑分开处理,这是因为首节点没有数据结构相关性。而后继节点需要判断是红黑树结构还是链表结构。总之在该阶段就是遍历节点,找到要删除的节点,交给第三步
- 获取到要删除的节点,首先根据传入方法的参数确定是否需要匹配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的容量,既常说的capacitythreshold
就是临界值,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,看是否要将链表转换成红黑树,最后打破循环,最后returnnull
- 遍历在寻找尾节点过程中,发现有相同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源码分析相关推荐
- 【Java深入研究】9、HashMap源码解析(jdk 1.8)
一.HashMap概述 HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现.与HashTable主要区别为不支持同步和允许null作为key和value.由于HashMap不是线程 ...
- 【Java自顶向下】面试官:HashMap源码看过吗?我:看过!面试官:好极了,那么来扒一扒吧!
HashMap 关于hash表的基础内容,请看文章 [数据结构-查找]3.散列表详解 [Java自顶向下]HashMap面试题(2021最新版) 顶层应用 public class HashMapTe ...
- hashmap remove 没释放内存_java从零开始手写 redis(13)HashMap 源码原理详解
为什么学习 HashMap 源码? 作为一名 java 开发,基本上最常用的数据结构就是 HashMap 和 List,jdk 的 HashMap 设计还是非常值得深入学习的. 无论是在面试还是工作中 ...
- Java8 HashMap源码分析
前言 今天,我们主要来研究一下在Java8中HashMap的数据结构及一些重要方法的具体实现. 研究HashMap的源代码之前,我们首先来研究一下常用的三种数据结构:数组.链表和红黑树. ...
- Java源码详解二:HashMap源码分析--openjdk java 11源码
文章目录 HashMap.java介绍 1.HashMap的get和put操作平均时间复杂度和最坏时间复杂度 2.为什么链表长度超过8才转换为红黑树 3.红黑树中的节点如何排序 本系列是Java详解, ...
- 【Java源码分析】Java8的ArrayList源码分析
Java8的ArrayList源码分析 源码分析 ArrayList类的定义 字段属性 构造函数 trimToSize()函数 Capacity容量相关的函数,比如扩容 List大小和是否为空 con ...
- [Java] HashMap 源码简要分析
特性 * 允许null作为key/value. * 不保证按照插入的顺序输出.使用hash构造的映射一般来讲是无序的. * 非线程安全. * 内部原理与Hashtable类似. 源码简要分析 publ ...
- Java类集框架 —— HashMap源码分析
HashMap是基于Map的键值对映射表,底层是通过数组.链表.红黑树(JDK1.8加入)来实现的. HashMap结构 HashMap中存储元素,是将key和value封装成了一个Node,先以一个 ...
- java基础之HashMap源码分析
目录 1. HashMap原理分析 1.1. HashMap继承体系 1.2.Node数据结构分析 1.3.底层储存结构 1.3.1.put方法分析 1.4.hash碰撞 1.4.1.key值的唯一性 ...
最新文章
- C++ Opengl纹理贴图源码
- 二十四、数据挖掘时序模式
- struts2几种result type探究
- 我的世界java版和基岩版对比_我的世界:基岩版比Java多出的七个特性,都听过的非老mc莫属了!...
- 970页绝版资料!初高中数学与竞赛知识点+方法技巧,由苏步青当顾问,众多一线名师共同编写!...
- 请对比html与css的异同,css3与css2的区别是什么?
- Java实现连连看源代码文档_Java实现游戏连连看(有源代码)
- swift建立桥接_在Swift中建立Alexa技能
- python中redirect_详解如何用django实现redirect的几种方法总结
- linux7配网卡,CentOS 7 配置网卡
- STM32F103单片机JTAG端口重映射
- JS 获取元素当前的样式信息
- 算法笔记_面试题_18.动态规划_模板及示例十几道(上)
- Java GC种类以及触发时机
- hasp运行不成功_HASP加密狗驱动程序没有安装成功如何解决
- C++_SHFileOperation文件夹操作
- 让你的工作变轻松的一套免费的 iPhone 手势图标
- 转 解决打印机无法打印的方法
- Spark中广播变量(boardcast)的使用
- 2022-2028年全球及中国红外(IR)传感器行业投资前景分析
热门文章
- Flash制作简单塔防游戏(一)
- js浮动运动函数html,JS+CSS动态绘制元素曲线运动轨迹(数学函数)
- 单引号和双引号的区别(字符串字面量 字符常量) | C语言
- UG476-Xilinx-7Series-FPGA高速收发器使用学习—RX接收端介绍
- android 自动生成id,《转载》生成Android设备的唯一ID
- 微众银行2021前端实习生笔试
- java输出数组中的元素_java一行代码输出数组的所有元素内容
- 2022年 change detection遥感图像变化检测 论文附代码
- 智能硬件的时代划分:如何向外行装逼
- QQ浏览器极速内核关闭“您即将提交的信息不安全”提示