标签:only 类型 bst existing 执行 就是 method shell 结构
HashMap通过继承AbstractMap
实现了Map
接口,且本身也实现了Map
接口
在接口实现关系上来看为多余操作
但有一点要注意,在使用反射获取实现接口时,如果不是显示实现某接口而是通过继承来实现该接口,则不会获取该接口类型,这一点在使用动态代理时要注意
HashMap.class.getInterfaces()//[interface java.util.Map, interface java.lang.Cloneable, interface java.io.Serializable]
HashMap实现了Serializable
接口,可以通过java.io.ObjectStream
进行序列化
HashMap实现了Cloneable
接口,实现了浅拷贝,通过以下代码可以证实:
HashMap map = new HashMap();
map.put(1, "first");
HashMap copiedMap = (HashMap) map.clone();
System.out.println(copiedMap.get(1)==map.get(1));//true
copiedMap.put(2, "second");
System.out.println(map);//{1=first}
(1)哈希表
哈希表也成为散列表,它是根据关键字经过hash()
函数处理,将值映射到哈希表上的某一个位置,以该位置作为关键字的存储位置,无论存在哪,只需要进行一次hash计算,便可以找到该位置,整个查找过程时间复杂度为\(O(1)\)
HashMap 使用数组作为哈希表,来存储Key的hash()信息
(2)链表
因为hash()
函数是将key值映射到有限的空间中,如果hash()
函数碰撞性设计的不完善,或者哈希表存储的元素过多,必然会导致不同元素的hash值相同,即位置冲突,此时我们采用的方式一般有:
hash()
函数映射关系使元素分布地更加均匀,降低碰撞几率(3)红黑树
红黑树为一颗平衡二叉树
当元素越来越多时,hash碰撞也会越来越严重,链表的长度会变得很大,此时如果我们想要查找该链表的的某一个元素,其时间复杂度为\(O(n)\),必须采用一个阈值,当链表的长度达到该阈值时,便应该将链表转化为一颗红黑树,从而将时间复杂度从\(O(n)\)降低为\(O(logn)\),从而改善Hash表的查找性能
(1)缺省table长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//为16
(2)table长度的最大值
static final int MAXIMUM_CAPACITY = 1 << 30;
(3)缺省负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
(4)树化阈值-链表长度
static final int TREEIFY_THRESHOLD = 8;
(5)树化阈值-HashMap元素个数
static final int MIN_TREEIFY_CAPACITY = 64;
(6)树降为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
(1)HashMap元素个数
transient int size;
(2)Hash表结构修改次数
transient int modCount;
(3)扩容阈值
int threshold;
threshold = loadFactor * capacity
(4)负载因子
final float loadFactor;
(1)静态结点类源码
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
. . . 构造方法
. . . getter&&setter
//重写的hash()方法
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//重写的equals方法
public final boolean equals(Object o) {
//如果为该对象
if (o == this)
return true;
//如果是Map.Entry类型
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
//如键,值equals方法都为真
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
...toString(){...}
}
结点Node类为实现了Map.Entry
接口的一个静态内部类,主要包含:
hash
计算值:后面构造时用以保存hashcode,避免每次重新计算key
:键value
:值next
:指向下一个结点的引用:如果发生哈希碰撞,使用链式结构存储hash值相同的键值对重写的hash()
方法将key和value经Objects.hashCode(Object o)
处理后进行与操作,使得hash()
函数映射更加随机均匀
重写的equals
方法中只有传入结点为自身结点或者key
与value
调用equals方法比较都为真时,结果才为真(主要用于后面查找元素时的比较)
(1)Hash表源码
Hash表定义源码:
transient Node<K,V>[] table;
为了能够通过确定计算的hashcod找到桶的位置,HashMap中底层采用Node<K,V>结点类型数组作为桶
transient修饰在序列化时会被跳过
(2)确定桶的位置
put
一个结点时,通过以下算法来确定结点应该位于哪个桶
index = (table.length - 1) & node.hash
,也就是说位置完全取决于node.hash
的后几位(table.length-1的二进制位数)(3)长度的确定
hash表长度计算方法源码:
static final int tableSizeFor(int cap) {
int n = cap;
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;
}
该方法的作用是,当初始化时,通过传入capacity返回一个大于等于当前值cap的一个数字,并且这个数字一定是2的次方数
该方法不断通过右移然后进行‘或’操作,可以将传入的cap中的首非0位之后的0位填满,变为0..11...1
的形式,即\(2^{n的二进制位数+1}-1\)
例:
cap = 10
n = 10 - 1 => 9
0b1001 | 0b0100 => 0b1101
0b1101 | 0b0011 => 0b1111
0b1111 | 0b0000 => 0b1111 = 15
return 15 + 1 = 16
cap = 16
n = 16;
0b10000 | 0b01000 =>0b11000
0b11000 | 0b00110 =>0b11110
0b11110 | 0b00001 =>0b11111
return 31 + 1 = 32;
Hash表的长度必须为2的整数次幂
(4)hash值的计算:hash(K key)
hash扰动函数源码:(实际上产生的就是Node.hash
,用于后面Node对象的构造)
static final int hash(Object key) {
int h;//h=0
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
如果key为null则hash值为0,位置计算结果也为0(从putVal方法的第一种情况可以看出),会替换掉table[0]
桶位的key为null的元素
当table长度比较小时,从前面位置算法index = (table.length-1)|node.hash()
可以看出,如果不经处理,计算的index只取决于结点的hash的后几位(table.length-1的二进制长度),这样会使不同元素的位置结果相同概率大大增加
h>>16的结果的高16位全为0,(h = key.hashCode()) ^ (h >>> 16)
就可以让结果hash的高16位为key.hashCode()的高16位,使得高16位也参与hash值的计算,增加低16位的随机性
例:
h = key.hashCode() = 0b 0010 0101 1010 1100 0011 1111 0010 1110
0b 0010 0101 1010 1100 _ 0011 1111 0010 1110
^
0b 0000 0000 0000 0000 _ 0010 0101 1010 1100 (h>>>16)
=> 0010 0101 1010 1100 _ 0001 1010 1000 0010
(1)HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity, float loadFactor) {
//前面都是校验传入参数是否合法,不合法抛出 IllegalArgumentException
//initialCapacity必须是大于0 ,最大值为 MAX_CAPACITY
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: "
+initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//loadFactor必须大于0且不是NaN
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
this.loadFactor = loadFactor;
//HashMap的扩容阈值通过以下方法取得
this.threshold = tableSizeFor(initialCapacity);
}
(2)其它构造方法
其它构造方法源码
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 0.75
//现在的扩容阈值thershold = 0
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
//现在的扩容阈值thershold = 0
//调用putMapEntries,添加键值对s
putMapEntries(m, false);
}
putMapEntries
方法源码:
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//获取键值对map长度
int s = m.size();
if (s > 0) {
//如果table没有初始化,先计算一系列属性再初始化
if (table == null) { // pre-size
//计算初始的table长度
float ft = ((float)s / loadFactor) + 1.0F;
//选取table长度与最大长度的最小值
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
//如果map的长度大于扩容阈值,扩容table
else if (s > threshold)
resize();
//遍历调用put方法添加键值对
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.entrySet()
,依次插入键值对(1)put(K key, V value)
源码:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
套娃方法,该方法首先计算调用hash()
方法计算hash值(并不是key.hashCode()),调用putVal
以替换的方式进行结点插入操作,返回之前结点的value
(2)putIfAbsent
源码:
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
套娃方法,计算hash值,调用putVal
以非替换的方式进行结点插入操作,返回之前结点的value
(3)putAll
源码:
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
套娃方法,调用putMapEntries(m, true)
实现添加传入的map的所有的键值对
(3)putVal
putVal源码
/**
* Implements Map.put and related methods.
*
* @param hash hash for key -key的hash值
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don‘t change existing value -true:如果该元素已经存在与HashMap中就不操作了
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
//tab:引用当前hashMap的散列表
//p:表示当前散列表的元素
//n:表示散列表数组的长度
//i:表示路由寻址结果
Node<K,V>[] tab; Node<K,V> p; int n, i;
//延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中的最耗费内存的散列表
//这样会防止new出来HashMap对象之后却不存数据,导致空间浪费的情况
if ((tab = table) == null || (n = tab.length) == 0)//此处进行tab与n的赋值
n = (tab = resize()).length;
//情形1:寻址找到的桶位,赋值给p,如果p刚好是 null,这个时候,直接将当前k-v=>node 扔进去就可以了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//e:临时的Node元素,不为null的话,找到了一个与当前要插入的key-value一致的key的元素
//k:表示临时的一个key
Node<K,V> e; K k;
//情形2:表示桶位中的该元素,与你当前插入的元素的key完全一致,表示后续需要进行替换操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//情形3:如果改桶为存储的结点与插入结点的hash不同或者key不一致吗,且为红黑树的树根结点,在需要插入结点到红黑树中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//情形4:链表的情况,而且链表的头元素与我们要插入的key不一致。
else {
for (int binCount = 0; ; ++binCount) {
//条件成立的话,说明迭代到最后一个元素了,也没找到一个与你要插入的key一致的node,说明需要加入到当前链表的末尾
if ((e = p.next) == null) {
//链表末尾添加新的结点
p.next = newNode(hash, key, value, null);
//条件成立的话,说明当前链表的长度,达到树化标准了,需要进行树化
//TREEIFY_THRESHOLD - 1 = 7
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//树化操作
treeifyBin(tab, hash);
break;
}
//条件成立的话,说明找到了相同key的node元素,需要进行替换操作
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//e不等于null,条件成立说明,找到了一个与你插入元素key完全一致的数据,需要进行替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//返回旧元素
return oldValue;
}
}
//modCount:表示散列表结构被修改的次数,替换Node元素的value不计数
++modCount;
//插入新元素,size自增,如果自增后的值大于扩容阈值,则触发扩容。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
整个方法的流程:
传入的参数
int hash
:上层调用者经过hash扰动后的hash值K key,V vey
:键,值boolean onlyIfAbsent
:如果为true,则对相同的元素不进行替换处理boolean evict
:没用,后面并没有做什么工作方法执行逻辑:
p
为null
Node e
,K k
p.hash值与传入的hash值相同
,并且p.key==key
或者说key通过equals方法比较为true
e=p
),下一步进行结点的值的替换p
为红黑树结点
p
开始遍历,binCount=0
,之后每次循环+1
(e=p.next)==null
,e
为当前结点p
的下一个结点),说明并没有找到该元素,new 新的结点插入到链表尾部(p.next = new Node(...)
)
binCount >= TREEIFY_THRESHOLD - 1
)),即当binCount到达7时,即插入操作后链表长度为8时(binCount
从0开始计数,而p开始就对应第一个结点),调用树化方法e.hash值与传入的hash值相同
,并且e.key==key
或者说equals方法比较为true
oldvalue = e.value
!onlyIfAbsent || oldValue == null
return oldvalue
;(源码中并没有方法体{没有内容},在这里没用)++size>thershold
,超过扩容阈值,则执行扩容方法return null
(1)扩容原因
(2)核心resize
方法
resize方法源码:
/**
* @return the table
*/
final Node<K,V>[] resize() {
//oldTab:引用扩容前的哈希表
Node<K,V>[] oldTab = table;
//oldCap:表示扩容之前table数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr:表示扩容之前的扩容阈值,触发本次扩容的阈值
int oldThr = threshold;
//newCap:扩容之后table数组的大小
//newThr:扩容之后,下次再次触发扩容的条件
int newCap, newThr = 0;
//条件如果成立说明 hashMap中的散列表已经初始化过了,这是一次正常扩容
if (oldCap > 0) {
//扩容之前的table数组大小已经达到 最大阈值后,则不扩容,且设置扩容条件为 int 最大值,返回原hashtable
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//oldCap左移一位实现数值翻倍,并且赋值给newCap, newCap 小于数组最大值限制 且 扩容之前的阈值 >= 16
//这种情况下,则 下一次扩容的阈值 等于当前阈值翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) \ newThr = oldThr << 1; // double threshold
}
//oldCap == 0,说明hashMap中的散列表是null
//1.new HashMap(initCap, loadFactor);
//2.new HashMap(initCap);
//3.new HashMap(map); 并且这个map有数据
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//oldCap == 0,oldThr == 0
//new HashMap();
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;//16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
}
//newThr为零时,通过newCap和loadFactor计算出一个newThr
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//创建出一个更长 更大的数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//说明,hashMap本次扩容之前,table不为null
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
//当前node节点
Node<K,V> e;
//说明当前桶位中有数据,但是数据具体是 单个数据,还是链表 还是 红黑树 并不知道
if ((e = oldTab[j]) != null) {
//方便JVM GC时回收内存
oldTab[j] = null;
//第一种情况:当前桶位只有一个元素,从未发生过碰撞,这情况 直接计算出当前元素应存放在 新数组中的位置,然后
//扔进去就可以了
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//第二种情况:当前节点已经树化,本期先不讲,下一期讲,红黑树。QQ群:865-373-238
else if (e instanceof TreeNode)
((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;
//hash-> .... 1 1111
//hash-> .... 0 1111
// 0b 10000
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
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;
}
确定新的表容量以及新的扩容阈值逻辑:
MAXIMUN_CAPCITY=1<<30
,则将扩容阈值设为整数最大值,返回原table(已经无法扩容了)newCap = oldCap<<1
,如果newCap小于MAXIMUN_CAPCITY
并且旧的负载oldCap大于默认表容量16,则新的扩容阈值等于旧的扩容阈值翻倍oldThr>0
(构造HashMap时除了无参默认方法其他都直接或间接设置了thorshold),让新的表容量等于旧的扩容阈值newCap=oldThr
oldCap == 0,oldThr == 0
(使用无参构造方法),让newCap = 默认表容量16
,newThr = 默认负载因子*默认表容量
开始将旧表元素分配到新的表上
使用newCap
创建新的数组newTab
如果:oldTab!=null,说明扩容之前数组就已经被创建了,此时开始遍历oldTab.for(j = 0;j<oldCap;++j):
如果:(Node e = oldTab[j]) != null
,即当前节点不为空:
令oldTab[j] = null
方便GC回收旧的数组
如果:e.next == null(即当前桶位只有一个元素),则让newTab[e.hash & (newCap - 1)] = e;
(利用新的表容量的位置算法,让e赋值到新表的相应位置)
如果:e为树的根结点,调用((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
去进行树中结点在新表的分配
如果:e为链表的头结点,创建4个链表结点loHead,loTail,hiHead,hiTail
index = e.hash & (15=1111b)
,也就是说是因为该链表的hash后4位相同,所以才分配到该链表上开始循环:创建结点next = e.next
(e.hash & oldCap) == 0
最后将两条链表的尾结点置null
将newTab[j] = loHead
,newTab[j+oldCap] = hiHead
元素的分配,扩容完成!!!
(1)remove(K)
方法
源码
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
套娃方法,调用removeNode方法,获取其返回的被删除结点,返回该结点的value
(2)remove(K,V)
方法
源码
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
套娃方法,调用removeNode方法,返回是否删除成功
(3)核心removeNode
方法
源码
/**
* Implements Map.remove and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue if true only remove if value is equal
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//tab:引用当前hashMap中的散列表
//p:当前node元素
//n:表示散列表数组长度
//index:表示寻址结果
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//说明路由的桶位是有数据的,需要进行查找操作,并且删除
//node:查找到的结果
//e:当前Node的下一个元素
Node<K,V> node = null, e; K k; V v;
//第一种情况:当前桶位中的元素 即为 你要删除的元素
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);
}
}
//判断node不为空的话,说明按照key查找到需要删除的数据了
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//第一种情况:node是树节点,说明需要进行树节点移除操作
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//第二种情况:桶位元素即为查找结果,则将该元素的下一个元素放至桶位中
else if (node == p)
tab[index] = node.next;
else
//第三种情况:将当前元素p的下一个元素 设置成 要删除元素的 下一个元素。
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
方法逻辑:
removeTreeNode(this, tab, movable)
方法,删除并保存树的结点(1)replace(K,V)
源码:
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
调用getNode
方法通过比较key来找到需要替换的结点,将value赋给该结点,返回旧value;如果没找到返回null
(2)replace(K key, V oldValue, V newValue)
源码:
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
调用getNode
方法通过比较key与value来找到需要替换的结点,将newValue赋给该结点,返回true,如果没找到返回false
(1)compute
方法
该方法实现了通过传入函数式接口,来对传入的key值对应的结点进行操作,如果找不到key对应的结点,会进行插入一个新的结点Node(key,mappingFunction.apply(key,value))
,并可能会重新排列部分数据结构;如果映射value结果为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;
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;
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;
}
方法逻辑
old
,如果找到了且value不为空,利用remappingFunction.apply(key, oldValue)
进行映射操作,获得新的value v否则:
v为null,调用removeNode(hash, key, null, false, true)
方法删除key,value对应结点t.putTreeVal(this, tab, hash, key, v)
实现作为新结点的插入使用方法
HashMap<Integer,String> hp = new HashMap<Integer,String>();
hp.put(1,"A");
hp.put(2,"B");
hp.put(3,"C")
hp.compute(2,(k,v)->v+"A")//将key为2的元素的value修改为value+"A"
hp ==> {1=A, 2=BA, 3=C}
hp.put(10,"A")
hp ==> {1=A, 2=BA, 3=C, 10=A}
hp.compute(10,(k,v)->null)//null
hp ==> {1=A, 2=BA, 3=C}
(2)computeIfAbsent
方法
public V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction) {...}
该方法如果没有检测到该key所对应结点,会添加一个新的结点Node(key,mappingFunction.apply(key,value))
到HashMap中,如果存在则不进行此操作,并返回null;如果映射value结果为null,则会删除该结点
使用方法
承接上处
hp.computeIfAbsent(2,(v)->"new value")//返回BA
hp ==> {1=A, 2=BA, 3=C}
jshell> hp.computeIfAbsent(4,(v)->"new value")//返回new value
hp ==> {1=A, 2=BA, 3=C, 4=new value}
(3)computeIfPresent
方法
public V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {...}
该方法如果检测到该key所对应结点,会添加一个新的结点Node(key,mappingFunction.apply(key,value))
到HashMap中,如果不存在则不进行此操作,并返回null;如果映射value结果为null,则会删除该结点
使用方法
承接上处
hp.computeIfPresent(5,(k,v)->"new value :5")//返回null
hp.computeIfPresent(4,(k,v)->"new value :4")//返回"new value :4"
hp ==> {1=A, 2=BA, 3=C, 4=new value :4}
(1)merge方法
该方法与compute类似,只不过以key与value
来寻找相应结点,找到则覆盖,找不到则插入新的结点Node(key,value)
如果oldValue为null或者节点不存在,则直接取value参数(不是映射后的value)作为节点的value值
同样的,作为特殊情况,如果计算出来的value值为null,删除节点
public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {...与compute类似...}
使用方法
HashMap<Integer,String> hp = new HashMap();
hp.put(1,"A");
hp.merge(1,"A",(k,v)->"new value");//new value hp ==> {1=new value}
hp.merge(1,"B",(k,v)->"new value:1")//new value:1 hp ==> {1=new value:1}
hp.merge(2,"B",(k,v)->"new value:B")//B hp ==> {1=new value:C, 2=B}
hp.put(null,"我是null")//映射后的默认值 hp ==> {null=映射后的默认值, 1=new value:C, 2=B}
hp.merge(100,"100的默认值",(k,v)->"映射后的100的默认值") //100的默认值 hp ==> {null=映射后的默认值, 1=new value:C, 2=B, 100=100的默认值}
(1)forEach
方法
源码
public void forEach(BiConsumer<? super K, ? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
。。。之后就是遍历table,对结点进行消费action.accpet(e.key,e.value)
}
(1)clear方法
源码
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
判断table是否初始化,如果不为空,每个位置置为null,等待JVM回收各个结点即可
(1)get
方法
源码:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
套娃方法,先用hash函数获取key的hash值,getNode去寻找Node结点,如果找到则返回value值,没找到则返回null
(2)getOrDefault
方法
源码:
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
套娃方法,和get方法类似,只不过如果找不到结点则返回传入的默认值
(2)核心getNode
方法
源码:
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
//tab:引用当前hashMap的散列表
//first:桶位中的头元素
//e:临时node元素
//n:table数组长度
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//第一种情况:定位出来的桶位元素 即为咱们要get的数据
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//说明当前桶位不止一个元素,可能 是链表 也可能是 红黑树
if ((e = first.next) != null) {
//第二种情况:桶位升级成了 红黑树
if (first instanceof TreeNode)//下一期说
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//第三种情况:桶位形成链表
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
方法逻辑
tab
表,头结点first
,临时结点e
,表长度n
first = tab[(n - 1) & hash]
不为空,则:
getTreeNode(hash, key)
寻找树种符合条件的结点,并返回结果(1)containsKey(K)
源码:
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
套娃方法,调用getNode
方法查找,返回是否查找到结点
(2)containsValue(V)
源码:
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
方法逻辑
(1)isEmpty
方法
return size == 0
,判断是否为空(2)size
方法
return this.size
,返回元素个数(1)HashMap中的序列化方法介绍
为了确保JVM的跨平台,防止因机器的不同序列化与反序列化的结果可能并不是正常构建的结果,HashMap中的数据属性很多都用transient
修饰,序列化时会被跳过,所以需要重写方法来自定义序列化的数据的写入与读取
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
HashMap中还为了方便,提供了序列化与反序列化方法(这里就不贴源码了,可以去jdk中看):
final private wirteObject(ObjectOutputStream)
final private readObject(ObjectOutputStream)
(2)方法作用
TreeNode
,对于值的遍历Values
类与key的遍历Keys
,EntrySet
等以及各种迭代器的设计,它们各自都有自己的数据操作方法,篇幅原因,就不一一赘述了标签:only 类型 bst existing 执行 就是 method shell 结构
原文地址:https://www.cnblogs.com/nishoushun/p/12686578.html