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位,所以是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个大模块:
- 计算数组扩容后的新容量和新临界值
- 根据新容量初始化新数组,即新位桶
- 将旧数组中的元素取出,拷贝至新数组
- 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的步骤是:
- 首先根据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上存放新节点元素,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的步骤:
- 判断是否是空集合,如果是初始化数组长度
- 不为空,则获取新元素要插入的位置,即在
table
中的位置 - 如果该位置上没有其他元素,则直接把新元素放在该位置上
- 如果该位置上已经有其他元素了,则新元素跟该位置上的元素进行比较,是否是同一个元素
- 如果是直接更新
value
,如果不是则判断该位置的数据结构是链表还是红黑树 - 如果是红黑树则执行红黑树的
putTreeVal
方法,如果是链表则循环链表找到尾节点 - 如果尾节点的下一个节点是
null
,就直接在next上存放新元素,插完节点后还要判断链表长度是否大于等于8,看是否要将链表转换成红黑树,最后打破循环,最后returnnull
- 在找尾节点的过程中,如果其中一个节点跟新元素是同一元素,即key相等,则打破循环
- 在寻找尾节点过程中,发现有相同元素而打破
for
循环,会执行新值替换旧值方法,返回旧值
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;
}
}
-
删除节点的步骤:
- 首先判断集合中是否存在该key的hashcode的节点位置,有则继续,没有则没有匹配项
- 如果有位桶数组存在该索引,则对首节点和后继节点的判断逻辑分开处理,这是因为首节点没有数据结构相关性。而后继节点需要判断是红黑树结构还是链表结构。总之在该阶段就是遍历节点,找到要删除的节点,交给第三步
- 获取到要删除的节点,首先根据传入方法的参数确定是否需要匹配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的容量,既常说的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位进行中和
为什么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;
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可以有以上的规律进行优化