HashMap源码详细解读(2万字加图文)
- 构造方法
- 一、静态代码块(Static utilities)
- 二、 关键字段(Fields)
- 三、 公共方法(Public operations)
- HashMap的get方法 public V get(Object key)
- HashMap的put方法 public V put(K key, V value)
- HashMap的resize()方法 final Node<K,V>[] resize()
- HashMap的remove()方法 public V remove(Object key)
- HashMap中重载了JDK8的一些方法
- 如果不存在就计算 public V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction)方法
- 如果存在就计算 public V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction)方法
- 计算方法 public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) 方法
- 合并方法 public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)方法
- 增强for循环 public void forEach(BiConsumer<? super K, ? super V> action) 方法
构造方法
阅读HashMap源代码,首先看他的构造方法,大概知道要怎么用他:
构造方法有4个:
// 设置初始化容量和loadFactor2个参数,map还是空的
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
// 用的最多的构造方法,他只是将loadFactor设置了默认值,其他的字段都设置成本来的默认值,map还是空的
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 设置一个初始化容量大小,map还是空的
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// loadFactor设置了默认值,同时向集合中放入一些数据,map就不再是空的了
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
以上4个构造方法里面本质上是在初始化2个参数,负载因子loadFactor
和阈值threshold
。
这里有2个地方需要说明一下
第一个,从构造方法HashMap(int initialCapacity, float loadFactor)的实现可以看出来,HashMap的最大初始化容量是static final int MAXIMUM_CAPACITY = 1 << 30;
1<<30 的值是2的30次方即1073741824;最小容量是0。
第二个,就是初始化容量时调用的tableSizeFor()方法,这个方法会把传入的参数“格式化”成与传入参数最接近的2的N次方,比如:tableSizeFor(15) = 16 ,tableSizeFor(1000) = 1024 ,方法具体说明在后面。
构造方法看完之后我们观察一下代码,会发现作者把代码分成了若干个部分,按作者的注释分成了:
- 静态代码块 (/* ---------------- Static utilities -------------- */)
- 字段定义(/* ---------------- Fields -------------- */)
- 公共方法区(/* ---------------- Public operations -------------- */)
- 克隆和序列化(// Cloning and serialization)
- 迭代器(// iterators)
- 拆分器(// spliterators)
- 对LinkedHashMap的支持(// LinkedHashMap support)
- 树操作的封装(// Tree bins)
- 红黑树的操作方法,左旋、右旋、平衡树插入等(// Red-black tree methods, all adapted from CLR)
一、静态代码块(Static utilities)
静态代码块中有4个方法:`int hash(Object key)、comparableClassFor(Object x)、int compareComparables(Class<?> kc, Object k, Object x)、int tableSizeFor(int cap)`
hash(Object key) 方法
看方法注释,第一句Computes key.hashCode() and spreads (XORs) higher bits of hash to lower 计算key的hashCode,并且通过XOR(异或)的方式把高位传到低位。为什么要这样计算,注释后面详细的说明了这么做是为了减少hash碰撞,使得计算出来的hash值能够更加好的分布在数组上。具体操作就是将key的hash值与其无符号右移16位后的值进行异或运算。
使用(h = key.hashCode()) ^ (h >>> 16)
计算hash值的根本原因?
hash值的计算的目的是为了决定当前这个Node应该放在数组的哪个位置上,从HashMap的putVal方法中可以看到计算方式实际上是将计算出的key的hash值与数组长度减去1取与运算(i = (n - 1) & hash
)得出位置的,也就是说甭管你hash值有多大,最终起作用的实际上只有n-1那一段,所以如果不去将hash值高位与低们做一定的运算的话,hash值前面一大截相当于用不起作用了,另外与(&)、或(|)操作都会使得结果偏向0或者1 ,所以用异或(^)运算一下,当然设计一个更高级方式运算也不是不可以,但是这里运算的目的仅仅是为了让高位参与一下运算以便根据hash值计算数组位置时能稍微更分散一些,没必要搞的那么复杂。画图示意一下:
这里注意几点:
- 首先这里计算的是Node对象中的key的hash值;
- 如果key为null,他会返回个0,说明如果HashMap的key值为null,会默认计算(写死)他的hash值是0,把他放到第0个位置上面;
- 如果key不是null,则调用他的hashCode()方法,并将hashCode()方法计算出的值的高位与低位进行一个异或运算从而得出最终的hash值。
Class<?> comparableClassFor(Object x)方法
从注释可以看出来这个方法的作用是返回一个类类型,如果这个类实现了Comparable接口的话。
int compareComparables(Class<?> kc, Object k, Object x)方法
返回k和x比较的结果
int tableSizeFor(int cap) 根据给定值计算HashMap容量大小
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;
}
此方法的目的是不论传什么值进来,最终返回的容量大小都是2的N次方(比如:tableSizeFor(5)=8、tableSizeFor(1025)=2048),此方法的计算方式如下:
为什么这里要先把cap减个1再去作与运算?因为不论传多大的值进来只要他不大于最大值MAXIMUM_CAPACITY static final int MAXIMUM_CAPACITY = 1 << 30;
经过最多5次与操作运算后得到的结果二进制的表达形式一定是所有位上都是1,即计算结果一定是111…111,所以最后要进行加1操作结果才会是2的N次方,所以才提前去减1。
二、 关键字段(Fields)
除了上面构造方法中提到的2个字段外,Fields下面还有4个字段,Node类型的table数组,Set类型的entrySet集合,HashMap大小size以及HashMap结构修改次数modCount(modified count 修改次数)
Node<K,V>[] table 节点
此节点乃是真正存放数据的节点,Node对象的定义如下:
他包含了key、value、hash值和指向下一个节点的next节点对象。这个对象中只有一个next字段,说明HashMap中的链表是单向链表。
Set<Map.Entry<K,V>> entrySet集合
主要用于保存缓存的entrySet()。
size就是当前HashMap的大小
modCount HashMap结构修改次数
结构修改是指改变HashMap中映射的数量或以其他方式修改其内部结构。该字段用于使HashMap的collection -view上的迭代器快速失败。
三、 公共方法(Public operations)
公共方法中构造方法已经看过了,再往下是几个简单的方法int size()
和boolean isEmpty()
。size()方法直接返回了类中的变量size的值,说明HashMap在变化的时候他的size值肯定是一起在变化的。所以isEnpty()方法也可以通过size的值来作判断。再往下是HashMap中最核心的方法之一,get(Object key)方法。
HashMap的get方法 public V get(Object key)
HashMap的get方法中调用了getNode()方法,如果取到了Node,则返回Node节点中的value值,否则返回null。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode()方法传了2个参数,一个是根据key值计算的hash值,一个是key,所以看一下getNode()方法:
final Node<K,V> getNode(int hash, Object key) {
// 定义变量
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 给变量赋值,
//tab赋值当前的hash表,n赋值为数组的长度,first根据hash值与数组长度减1取与操作,计算出key值在数组中的位置,所以取变量叫first
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 如果刚好first节点就是要取的值(hash一致,key值一致),直接返回first
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果fisrt节点不是要取的值,并且下个节点不为空,就有2种情况了,此时根据hash值计算的节点要下有可能是个链表也有可能是个红黑树
if ((e = first.next) != null) {
// 如果first节点是TreeNode类型的,说明他已经是红黑树了,此时就调用树的查询方法去取值,
// 注意TreeNode实际上继承了HashMap中的Node类的所以才直接返回
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 不是TreeNode类型的话就肯定是链表了,链表的话就直接不停的next去查询就可以了
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 最后查不到,则会返回null
return null;
}
所以HashMap的get方法总结一下就是:
- 根据key值计算出来hash值;
- 然后根据hash值和数组长度计算当前这个key对应的Node应该在数组的哪个节点上,并判断数组上的第一个元素是不是刚好就是要找的,如果刚好是要找的就直接返回;
- 如果不是要找的那么肯定hash冲突了,所以这个节点上有可能是个红黑树或者是个链表;
- 判断是不是树结构,是的话根据树节点的查找方法去查询;
- 不是的话那肯定就是链表了,直接遍历链表。
HashMap的put方法 public V put(K key, V value)
put方法里面调用了putVal()方法,
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
真正核心的是putVal()方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
// 根get方法类似的,定义要用变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 当数组为空或者数组长度为0的时候要扩容
if ((tab = table) == null || (n = tab.length) == 0)
// 调用resize()方法扩容,并将容量(数组长度)赋值给n
n = (tab = resize()).length;
// 根据数组长度和hash值计算应该放在数组的哪个节点上,同时判断节点如果不为空的话就将节点赋值到变量p上
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果节点为空的话,直接将当前的对象放到这个节点上(即new一个Node)
tab[i] = newNode(hash, key, value, null);
// 当前这个节点不为空的情况下
else {
Node<K,V> e; K k;
// 判断p的hash值与key值是不是与当前计算的一致,一致就将p赋值给变更e,这里跟get方法中的找fisrt是一样的
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 不一致的话判断p是不是树类型的,如果是树类型的,则调用树的put方法去往树上加节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 不是树类型的那就是链表了,链表就麻烦点了
else {
// 从0开始循环,for循环中间没有条件
for (int binCount = 0; ; ++binCount) {
// 找到链表上节点的next节点是null的节点,将新添加的对象放到next节点上去
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果链表的长度大于或8了,将就链表转成红黑树,binCount的值从0开始,计算到7的时候链表长度为8
// TREEIFY_THRESHOLD 默认值是 8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果key在HashMap中已经存在了,就跳出循环遍历
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果key值在HashMap中已经存在,那么putVal()方法会返回已存在的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 完成添加元素后修改次数+1
++modCount;
// HashMap的size大小+1,加1后判断当前大小有没有达到阈值,如果达到了就要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put方法总结一下就是:
- 进来先判断一下表是不是为空,如果为空就调用resize()方法给他扩容一下;
- 根据数组长度和hash值计算应该放在数组的哪个位置上,如果这个位置刚好是空的那就直接把对象放上去;
- 如果不为空,那么就跟get方法类似,得先判断一下当前这个数组位置上保存的节点是树(TreeNode)类型还是链表(Node)类型;
- 如果是树就调用树的putTreeVal()方法去树上加一个节点,树的方法比较复杂可能涉及到红黑树的旋转操作;
- 如果是链表,就在链表最末端添加新元素;
- 添加完新元素后判断链表长度是不是大于8了,如果是就将链表转成红黑树;
- 添加时如果key已经存在,则返回原来的value值;
- 添加完成后修改次数加1;
- HashMap的size大小加1,加1后判断当前大小有没有达到阈值,如果达到了就要扩容。
HashMap的resize()方法 final Node<K,V>[] resize()
在putVal()方法中多次提及了resize()方法,resize()方法的作用他注释上写了:Initializes or doubles table size 初始化或者将表扩大两倍。可以看到resize()方法他是没有参数的,所以他肯定是只能固定的扩大,比如注释里面说的初始化(用默认参数进行初始化)、扩大两倍。如果要扩大3倍、5倍那是做不到的。看源代码。
final Node<K,V>[] resize() {
// oldTab参数赋值
Node<K,V>[] oldTab = table;
// 旧的容量赋值,如果oldTab为空那就是0,否则就是他的数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 旧的阈值赋值
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 如果旧的容量值大于等于最大容量值(MAXIMUM_CAPACITY=1<<30 不可能大于,只可能等于),那就没救了,扩不了空了,所以直接返回旧的表
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 新的容量值等于旧的乘以2,他也得小于最大容量值,同时如果旧的容量值大于默认初始化容量值(1<<4 = 16)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新的阈值等于旧的阈值乘以2
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 使用默认参数进行初始化,仅仅在为0的时候才作这个初始化操作
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果一通操作下来,新的阈值为0的话,就自己计算一下新的阈值,公式是新阈值=新的容量*加载因子,ag: newThr = 16 * 0.75 = 12
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 最后把计算的新的阈值给赋值回参数 threshold 上。
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 定义新长度的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 用旧的容量长度来遍历循环
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 如果元素的next为空,说明只有数组上有一个节点,这个节点上没有发生过hash冲突
if (e.next == null)
// 那么就直接用这个位置上的元素的hash值和新的数组长度来重新计算这个元素要放到新的数组中的位置
newTab[e.hash & (newCap - 1)] = e;
// e.next上不为空,并且e的类类型是TreeNode,说明这个位置已经是红黑树了,
else if (e instanceof TreeNode)
// 如果是红黑树,就将树拆分成高位树hi和低位树lo,拆完后分别判断2个树的大小(与UNTREEIFY_THRESHOLD = 6相比)如果小于
// 等于6则把树转成链表,否则就把半截树树化(旋转)一下,转完之后低位树(或链表)放在与原来位置相等的位置上,高位树放
// 在原来的位置加老的数组长度的位置上,如下图所示。
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 如果下一个元素有值但是不是红黑树那么肯定是链表
else { // preserve order
// 定义变量,低位的首尾节点
Node<K,V> loHead = null, loTail = null;
// 定义变量,高位的首尾节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 将e的hash值与旧的容量取与操作看他是不是为0,因为hash表的容量一定是2的N次方,所以oldCap转成2进制后一定是只有1
// 位上是1其他位上都是0,所以与hash值取与时只要容量位为1的位置对应的hash值的位置是0结果就是0,是1结果就非0。结
// 果为0的放在低位的链表上
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 结果为1的放在高位的链表上
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
}
// 具体的往链表上加节点的操作
while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
再往下看就是比较简单的一些方法了,比如public void putAll(Map<? extends K, ? extends V> m)
往HashMap里面put一个集合,这方法最终的实现还是靠调用putVal()
方法;下一个是remove(Object key)
方法,这个方法要看一下。
HashMap的remove()方法 public V remove(Object key)
可以看到,remove()方法具体的实现调用的是Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable)
方法。
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
}
先看他的参数,首先2个是根据key值计算出来的hash值和key,这2个参数不用多说,定位Node节点必须的2个参数。后面2个参数是value值和是否匹配value值的参数matchValue,这个值如果为true,则只有value值也匹配的上才删除(从方法调用上就能看到,value传的null,matchValue传的false,与这2个参数关系不大),最后一个参数movable,如果为false,则在移除节点时不移动其他节点(上面的调用传的是true,说明节点移除时可能会移动其他节点)。下面是removeNode()方法的源代码,我们来逐句分析:
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 与get方法一样的操作(要删除之前可不得先找到他):
// tab赋值当前的hash表,n赋值为数组的长度,p根据hash值与数组长度减1取与操作,计算出key值在数组中的位置
if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 如果刚好p节点就是要取的值(hash一致,key值一致),说明运气比较好,刚好是数组上那个位置的节点,后面就不用再遍历了
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 否则就得往下遍历了
else if ((e = p.next) != null) {
// 如果是红黑树,调用树的查找方法去找到对应的节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 不是树那就是链表,遍历链表找他
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 如果找到了节点并且满足后面的附加条件(此处matchValue = false,value = null)
if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
// 如果节点是树,则调用树的删除方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 如果节点是数组上那个位置的节点(相当于链表头),把链表的下一个节点放在数组上即删掉链表头
else if (node == p)
tab[index] = node.next;
// 再一种情况就是不是链表头,那就直接删掉这个节点,链表后面的往前移一个
else
p.next = node.next;
// 完成删除元素后修改次数+1
++modCount;
// 大小减1
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
要我说,这remove()方法上面半截查找Node节点的地方直接调用getNode()方法得了,他代码其实是一样的,也不知道为什么不这么做。移除这里是单个元素(没有hash冲突)或者是链表的都没什么,如果是红黑树的话要注意一个就是:添加的时候他是添加满了8个的时候会把链表转成红黑树,删除的时候是删到6个的时候会把红黑树又转成链表。
再往下又是几个比较简单的方法,清空方法public void clear()
直接size=0内容全部置为null,看指定的值是不是存在的方法boolean containsValue(Object value)
,取所有key的方法Set<K> keySet()
再往下又有需要重点关注的JDK8重载的一些方法:
HashMap中重载了JDK8的一些方法
前面的几个简单的就不说了,就是调用已有的方法,换了些参数传一下而已。
再往下就是几个lamada风格很明显的方法了:
如果不存在就计算 public V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction)方法
方法作用是如果在HashMap中找不到key值对应的节点,或者key对应的节点的value值为null,就把以key为key,后面的mappingFunction计算出来的值当作value保存到HashMap中去,如果找到了,就直接返回key值对应的value,看一下源代码:
@Override
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
// 计算函数他不能为空,否则要抛异常了
if (mappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
// 当数组上对应的类型是链表时,记录下遍历次数即链表长度
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
// 这里跟put方法类似,如果大小达到阈值或者数组为空或者数组长度为0的时候要扩容一下
if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 下面的一大段都是看过好几遍的代码了,他就是根据key来找Node,找到了就赋值给old这个变量
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
// 这里有一丢丢绕,遍历链表时如果找到了对应的节点就直接返回了节点的value值了,只有找不到的时候binCount参数才会起作用,此
// 时链表一定是遍历完了,所以binCount的值实际上只有要用到他的时候他才真正的起作用
++binCount;
} while ((e = e.next) != null);
}
V oldValue;
// 如果根据key找到了节点,并且节点value有值,直接返回节点对应的value值
if (old != null && (oldValue = old.value) != null) {
afterNodeAccess(old);
return oldValue;
}
}
// 函数计算得到value值
V v = mappingFunction.apply(key);
if (v == null) {
return null;
// old != null 然后代码能走到这里来,说明已找到的节点上value值是null,这里就把计算出的value赋值到节点上
} else if (old != null) {
old.value = v;
afterNodeAccess(old);
return v;
}
// 代码能走到这里来说明old=null, t!=null 说明节点是个树,所以就是从这个红黑树上查找没找到
else if (t != null)
// 调用树的添加方法把节点加上
t.putTreeVal(this, tab, hash, key, v);
// 这里t=null了,说明这里不是红黑树,那就是链表了(也有可能他就只有数组位置上有一个元素,即没有发生hash冲突)
else {
// 不论是链表还是只有数组位置上有一个元素,此处相当于都是在链表头插入元素
tab[i] = newNode(hash, key, v, first);
// 如果链表长度到了要树化的长度,就转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
// 修改次数加1
++modCount;
// 大小加1
++size;
afterNodeInsertion(true);
return v;
}
总结一下,这里的几个重点:
- 根据方法名,如果不存在就计算,这里的不存在有两层意思,一个是根据key值查找不存在,另一个是key值存在,但是value为空,这两种情况都会计算并添加;
- 根据参数的定义
Function<? super K, ? extends V> mappingFunction
这里的函数的入参类型一定是跟key一样,计算的返回值一定要跟value一样; - 如果不存在,这个方法实际上就跟put方法有点像,添加完成后也作了++modCount操作;
- 如果是链表的添加,这里跟put方法不一样的是put方法是在链表末尾添加,这里是在链表头添加。
如果存在就计算 public V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction)方法
这个方法跟上面那个反着来的,如果根据key值查找存在,就计算新的value值并替换掉原来的value值,但是这里要注意的是如果计算出来的新value值为null,他要直接把这个节点删掉。看一下源代码:
@Override
public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
// 计算函数不能为空,否则要抛异常了
if (remappingFunction == null)
throw new NullPointerException();
Node<K,V> e; V oldValue;
int hash = hash(key);
// 这里直接调用getNode()方法去查找
if ((e = getNode(hash, key)) != null &&
(oldValue = e.value) != null) {
// 这里的函数是个BiFunction,他可以传2个入参,计算新的value值
V v = remappingFunction.apply(key, oldValue);
// 计算的value如果不为空就把value替换一下并返回
if (v != null) {
e.value = v;
afterNodeAccess(e);
return v;
}
// 狠就狠在如果计算的结果是null,他要把这个节点干掉
else
removeNode(hash, key, null, false, true);
}
return null;
}
这里注意两点:
- 一个是这里的函数是可以传2个参数的BiFunction
BiFunction<? super K, ? super V, ? extends V>
2个参数前面一个跟key一个类型,后面一个跟value一个类型,返回值跟value一个类型; - 另一个是如果计算结果是null,这个节点就要被删除了。
计算方法 public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) 方法
这个计算方法算是集上面2个又方法于一体了,他是如果找不到并且计算的新value值不为null,就添加。如果找到了并且计算新value值不为null就替换,如果计算的值为null就干掉这个节点。看一下代码:
@Override
public V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
// 根据key值查找节点
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 TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
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;
// 函数计算新的value值
V v = remappingFunction.apply(key, oldValue);
// 如果根据key值查找到了节点
if (old != null) {
// 并且根据函数计算得出的值不为null
if (v != null) {
// 把函数计算出的值赋值给查出来的节点的value值
old.value = v;
afterNodeAccess(old);
}
// 计算出的结果如果是null,干掉查出来的节点
else
removeNode(hash, key, null, false, true);
}
// 根据key查不到节点,并且函数计算出的值不为空的时候就直接用key值和函数计算出来的值来添加一个新对象到HashMap(相当于调了put()方法)
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;
}
合并方法 public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)方法
合并方法的逻辑是:当根据key值找到了节点时,如果节点的value值不为null,则调用函数计算出一个新的value并赋值给v;如果节点的value为空则使用第二个参数里面传进去的value并赋值给v。上面2种操作之后,如果v值不为空就把v值赋值给刚刚找到的节点的value值上;如果v值仍然为空,那没救了,把节点删掉 。如果根据key值,找不到节点,那么就看参数中传的value是不是为空,如果不为空就用参数上的key和value添加到HashMap里面去,注意这里的添加跟comput类似,如果是链表的话是插入链表头。源码如下:
@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);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
// 这里判断一下是不是需要扩容,如果需要就扩容一下
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
// 下面这一部分是根据key查找节点
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
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;
// 并且节点的value不为空
if (old.value != null)
// 计算新的value值
v = remappingFunction.apply(old.value, value);
// 否则取参数中传的value值
else
v = value;
// 上面的2种情况下value如果不为空就把最终得出的结果赋值给节点的value
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
// 如果上面2种情况下还是为空那就没救了,把找到的节点干掉
else
removeNode(hash, key, null, false, true);
return v;
}
// 如果找不到节点,并且参数里面传的value不为空,就添加节点
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;
}
增强for循环 public void forEach(BiConsumer<? super K, ? super V> action) 方法
这是对Map的增强for循环,这里有两个点要注意,一个是参数是函数BiConsumer
,他传2个参数分别是key类型的和value类型的;另一个是在for循环之后,做了modCount是否有变化的判断,如果在for循环中有对HashMap作添加、移除、计算、合并等会影响modCount值的操作这里就会直接抛出ConcurrentModificationException
异常来快速失败。看代码:
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key, e.value);
}
// 看看modCount是否被改动了,一旦被改动了就抛出ConcurrentModificationException异常
if (modCount != mc)
throw new ConcurrentModificationException();
}
}