目录
HashMap采用key-value方式存储数据;
JDK1.7中HashMap底层采用Entry数组+链表结构存放数据,添加元素采用头插法。
JDK1.8开始采用Node数组+链表+红黑树结构存放数据,添加元素采用尾插法;当链表的元素个数达到8后,添加第9个元素时会先添加进去,然后判断所有元素>=64个,然后变成红黑树,若所有元素<64时,用数组扩容来替代变树;当树型的元素个数<=6时,红黑树就会变成链表结构。
一、属性
1.1 公有属性
DEFAULT_INITIAL_CAPACITY:默认的数组容量为16;
MAXIMUM_CAPACITY:设置的数组最大容量为2的30次方;
DEFAULT_LOAD_FACTOR:默认扩容因子(负载因子)为0.75。即元素达到数组长度的3/4时,再添加新元素就会触发扩容,选择0.75的原因是在0.5到1之间,0.75是最合适能够乘以2的n次方得到整数的小数;
TREEIFY_THRESHOLD:链表变成红黑树的阈值为8个元素,当超过阈值且所有元素总个数超过64就把链表变成树结构,若没超过64则采用扩容方式来代替变树;
UNTREEIFY_THRESHOLD:红黑树变成链表的阈值为6个元素,当树上的元素个数少于6个时,就把树变成链表结构;
注意:链表变树和树变链表的阈值不同,是为了防止在同一个临界点频繁切换而影响效率;
MIN_TREEIFY_CAPACITY:最小变成树的容量为64,即链表变成树时还需要数组长度(容量)>=64(为了防止前期阶段频繁扩容和树化过程冲突);
table:Node数组;即存储所有节点的数组;
size:所有节点的个数;
modCount:修改元素的次数(增、删、改),在多线程中根据该参数判断是否被修改,用于Fail-Fast机制;
threshold:扩容临界点(容量*扩容因子),当元素个数>扩容临界点时会扩容数组的容量(为原来的2倍);
loadFactor:扩容因子;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
transient Node<K,V>[] table;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
1.2 链表节点
Node:定义的一个静态内部类,将添加的元素封装为Node节点类,且赋予相应的属性;实现Map.Entry接口;
hash:存储当前节点根据key计算出的hash值(HashCode值);
key:存储当前节点的key值(名称);
value:存储当前节点的value值;
next:存储当前节点指向的下一节点地址;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//此处省略部分源码,该部分源码是实现Entry接口的方法
}
1.3 红黑树节点
TreeNode:一个静态内部类,根据情况将元素从Node节点变为TreeNode节点类,且赋予相应的属性;继承Node类;
parent:当前节点的上一节点;(父亲节点)
left:
right:
prev:
next:继承的Node类中属性,存储下一节点地址;
root():用于返回根节点(顶层节点);
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
二、构造方法
2.1 无参构造
当使用无参构造创建HashMap时,会给扩容因子初始化为默认值0.75;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
2.2 有参构造
有参构造相当于只有一个,因为当你需要指定容量创建HashMap时,底层会自动调用另一个有参构造,传入指定的容量和默认的扩容因子0.75;
使用有参构造创建HashMap时,初始化扩容临界点为>=所指定容量的二次方数(tableSizeFor方法返回),当指定了扩容因子时,会初始化扩容因子为所指定的参数;当没有指定时,则会初始化为默认的扩容因子;
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);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
2.2.1 tableSizeFor
该方法用来求出>=指定容量的最小的2的次方数,并返回。
为什么需要使用2的次方数来作为容量大小,其他的不允许吗?
不允许,一个是因为我们的扩容因子0.75*容量(2的次方数)是可以得到扩容临界点(整数);另一个就是因为求下标的时候要使用(数组容量-1)&hash替代%运算,提高效率。,
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
三、put方法
put方法是先把节点添加进去再进行判断是否达到变树的条件!!!
3.1 hash方法
hash方法传入的参数为当前操作节点的key值;
方法中优先判断key值是否为null,为null时返回计算后的hash值为0;
当不为null时,先将key值的hashCode值算出,并赋值给h变量,在通过无符号右移16位得到高16位(注意:int类型 占4byte 32位),然后与h进行异或运算。(JDK1.8通过右移然后异或让其高位也参与了hash值的运算,进一步降低hash冲突的几率)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
3.2 newNode
创建Node节点时使用该方法创建,new一个新的Node对象返回。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
3.3 put
添加元素,调用putVal方法,传入计算后key的hash值,key,value,false,true;
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
3.4 putVal
接收参数:
onlyIfAbsent:布尔类型,有相同元素时,为true时表示不更改value值;
evict:布尔类型,当为false时表示处于创建中;
为什么使用(n - 1) & hash来求出存放的数组下标(索引值)?
(数组长度-1)&hash只会求出0-(数组长度-1)的数,其实使用(数组长度-1)&hash等价于hash%数组长度求出的余数,因为位运算效率高所以采用&计算;
例如:数组长度为16,那么存放hash值为18的元素的下标为
(16-1) 0000 0000 0000 0000 0000 0000 1111
& 18 0000 0000 0000 0000 0000 0001 0010
存放下标为 0000 0000 0000 0000 0000 0000 0010(2)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//临时变量:tab用于存放数组,p存放当前节点,n用于存放数组此时长度(容量),i用于存放当前下标(索引值)
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//当添加元素,判断数组为null(没有元素)或数组没初始化长度(第一次添加元素),就先进行扩容数组,初始化tab和n
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//根据 (n - 1) & hash求出当前key应存放的数组下标,初始化p和i,判断数组该位置是否有元素,若没有就直接把添加的元素当做第一个元素,并转为node节点
tab[i] = newNode(hash, key, value, null);
//若数组中该下标有元素时
else {
//临时变量e存放下一节点,k存放下一节点的key值
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//判断第一个元素和添加元素的hash地址是否相同&&key是否指向同一个地址||(添加的key是否为null&&两个key是否相等)
e = p;
else if (p instanceof TreeNode)
//判断第一个节点是否为树节点,若是树节点就采用putTreeVal方法添加元素
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//若该下标有元素,且不是树结构,就遍历该下标所有节点
//binCount从0开始,用于记录当前下标有多少节点
for (int binCount = 0; ; ++binCount) {
//判断下一节点是否为null
if ((e = p.next) == null) {
//用于判断当前节点是否为尾结点,若是尾结点,就将添加的元素转为Node节点并存放到下一节点位置。
p.next = newNode(hash, key, value, null);
//判断节点的个数是否达到树的要求>=(8-1),这里>=7是因为for循环是从第二个节点开始判断且binCount=0,所以当binCount达到7是代表已经有了7+1个节点,新添加的为第9个节点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//treeifyBin方法进入后,还会接着判断所有节点个数是否<64,小于则扩容退出该方法
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//判断遍历到的节点是否相同,前面与第一个节点也是如此比较,相同则跳出循环
break;
//当前节点切换
p = e;
}
}
//判断是否存在相同的节点,有相同则进入
if (e != null) { // existing mapping for key
//记录原有的value值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//更新value值
e.value = value;
//空方法,该方法用于LinkedHashMap中
afterNodeAccess(e);
//返回原来的value
return oldValue;
}
}
//修改次数+1
++modCount;
//元素个数+1后判断是否大于扩容临界点
if (++size > threshold)
resize();
afterNodeInsertion(evict);
//添加元素成功后,返回值为null
return null;
}
3.5 resize
该方法用于扩容数组(原来数组的2倍),当第一次添加元素时会调用,或节点个数超过扩容临界点时会调用。
e.hash & (newCap - 1)和e.hash & oldCap和(数组长度-1)&hash?
三个运算是等价的,e.hash & oldCap只需要比较一位(因为oldCap一定是二的幂),而不用再取hash值的低位了加快了运算速度。
例如:当有一个数组容量为16,那么扩容后数组为32,那么(16-1)和(32-1)的二进制为(1111)和(11111),二进制差一位且为1,若e.hash从右向左第五位为0时,此时的e.hash在新老数组求出的下标位置都是相同的,若e.hash从右向左第五位1时,此时e.hash在新老数组求出的下标位置不同(相差从右向左第五位的1,也就是oldCap容量),所以我们只需要求出扩容前后数组相差的那一位在e.hash中的数字是0还是1就可得出下标位置。当为0时下标位置不变,当为1时用之前的下标+oldCap容量。
final Node<K,V>[] resize() {
//oldTab存放扩容前的数组
Node<K,V>[] oldTab = table;
//oldCap存放扩容前数组的长度(容量)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr存放扩容前的扩容临界点
int oldThr = threshold;
//newCap存放存放扩容后数组的长度(容量),newThr存放扩容后数组的扩容临界点
int newCap, newThr = 0;
if (oldCap > 0) {
//判断老数组长度是否为0(第一次添加元素时长度为0)
if (oldCap >= MAXIMUM_CAPACITY) {
//判断老数组长度是否超过最大容量
//超过就将int最大值设置为扩容临界点,让其无法再扩容
threshold = Integer.MAX_VALUE;
//返回老数组,因为无法扩容
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//判断新数组(老数组2倍)是否规范,初始化newCap,在默认的容量(16)和最大容量(2的30次方)之间,在就直接扩容2倍到newThr
newThr = oldThr << 1; // double threshold
}
//当第一次添加元素时,判断threshold是否初始化(有参构造初始化了,无参构造没有)
else if (oldThr > 0) // initial capacity was placed in threshold
//有参构造第一次添加元素,将新数组容量初始化为扩容临界点值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//无参构造第一次添加元素,则新数组的容量和扩容临界点都设置为默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//判断新数组有无扩容临界点(针对于有参构造第一次添加元素或新数组扩容后不规范)
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
//求出新的扩容临界点并判断是否规范(新数组容量是否超出最大容量,扩容临界点是否超出最大容量)
//若规范就将新数组扩容临界点设置为新求的扩容临界点,若不规范,就将int最大值设置为新数组扩容临界点,使其无法再次扩容
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//新数组扩容临界点赋值到公有属性threshold
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//new出新的数组到newTab
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//newTab赋值到公有属性table
table = newTab;
//判断老数组是否为null,若老数组有元素我们就需要将老数组元素都移动到新数组中
if (oldTab != null) {
//遍历老数组所有元素,并移动,j代表当前操作的下标
for (int j = 0; j < oldCap; ++j) {
//e存放当前节点
Node<K,V> e;
//判断老数组下标中第一个节点是否为null,null的话就不用移动到新数组,直接跳到下次循环
if ((e = oldTab[j]) != null) {
//将老数组的这个节点置为null,相当于移动到了新数组中,为了方便老数组被GC回收
oldTab[j] = null;
//判断当前下标是否只有一个节点(当前下标就一个节点)
if (e.next == null)
//若就一个节点,将其移动到e.hash & (newCap - 1)下标处然后跳到下次循环
newTab[e.hash & (newCap - 1)] = e;
//判断第一个节点是否为树节点
else if (e instanceof TreeNode)
//调用split方法分割树然后再移动
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//若不是树且有不止一个节点
else { // preserve order
//loHead存放下标属于低位链表头结点,loTail存放低位链表尾结点
Node<K,V> loHead = null, loTail = null;
//hiHead存放下标属于高位链表头结点,hiTail存放高位链表尾结点
Node<K,V> hiHead = null, hiTail = null;
//next存放下一节点
Node<K,V> next;
//遍历改下标所有节点
do {
next = e.next;
//比较扩容前后数组的相差的那位是0还是1,分成两个链表(一个存放e.hash相差那位为0的,一个存放为1的)
if ((e.hash & oldCap) == 0) {
//判断尾结点是否为空
if (loTail == null)
//将存放的第一个节点置为低位头结点
loHead = e;
else
//当前节点赋给尾结点的下一节点(在跳出循环后会将下一节点置为null,因为头结点不能改变所以只能使用尾结点一直连接后面的节点)
loTail.next = e;
//当前节点赋给尾结点
loTail = e;
}
else {
//原理与上面一样
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//判断低位链表是否为空
if (loTail != null) {
//将尾结点最后置为null然后把低位链表放到新数组下标不变的位置
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
//将尾结点最后置为null然后把高位链表放到新数组下标+oldCpa的位置
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回扩容后,且元素已经移动完成的新数组
return newTab;
}
3.6 treeifyBin
根据传入的hash,判断是否达到变树的条件,达到则变平衡的红黑树,没达到则用扩容代替变树。
final void treeifyBin(Node<K,V>[] tab, int hash) {
//n存放数组长度,index存放当前的数组下标,e存放当前节点
int n, index; Node<K,V> e;
//判断数组是否为null || 数组长度(容量)是否小于设置的最小树的容量64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//小于则用扩容替代树化(说明节点不是很多,只是节点都存放到了同一个位置)
resize();
//超过64则树化,求出hash存放的下标,判断是否有节点,初始化index,e
else if ((e = tab[index = (n - 1) & hash]) != null) {
//
TreeNode<K,V> hd = null, tl = null;
do {
//replacementTreeNode将当前节点转为树节点,且null传入next属性
//p中存放当前树节点
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
//当t1为null则表示当前尾结点为第一个树节点
hd = p;
else {
//当前树节点p的上一节点指向t1
p.prev = tl;
//上一树节点的下一节点属性指向p
tl.next = p;
}
//t1一直切换到当前树节点
tl = p;
} while ((e = e.next) != null);
//将第一个树节点存放到求出的index下标处,替换原来的链表,判断是否为null
if ((tab[index] = hd) != null)
//treeify方法得到树的根节点、判断红黑节点以及旋转红黑树使其平衡的红黑树
hd.treeify(tab);
}
}
3.7 split
移动红黑树。
扩容数组后,先分割红黑树,根据在新数组的下标分为低位和高位,然后判断分割后的低位和高位树是否达到变为链表的条件(树的节点个数<=6),达到则变为链表再存入新数组的对应下标处,若没达到就直接将两个树存入对应新数组的下标处。(基本与移动链表到新数组中一致)
传入参数:
map:hashMap对象
tab:新数组
idnex:正在遍历的旧数组下标
bit:扩容前的数组容量
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
//b存放调用split方法的对象(((TreeNode<K,V>)e)对象)
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
//loHead存放低位头节点,loTail存放低位尾结点
//hiHead存放高位头节点,hiTail存放高位尾结点。
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
//lc用于记录低位有多少个节点,hc记录高位。
int lc = 0, hc = 0;
//循环遍历当前下标处的所有树节点,e存放当前树节点next存放下一个树节点便于切换
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
//将当前节点的下一节点置为null
e.next = null;
//这里原理与链表移动时一样,可看resize方法中
if ((e.hash & bit) == 0) {
//判断是否为第一个树节点
if ((e.prev = loTail) == null)
//赋值第一个树节点
loHead = e;
else
//从loHead的next属性开始连接所有的树节点
loTail.next = e;
loTail = e;
//记录节点个数
++lc;
}
else {
//原理如上
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
//判断低位头节点是否为null,为null则表示没有低位,全都是高位
if (loHead != null) {
//判断是否达到树变链表的条件<=6
if (lc <= UNTREEIFY_THRESHOLD)
//达到就将对象变为链表
tab[index] = loHead.untreeify(map);
else {
//若没达到就将第一个节点赋值到新数组原下标处
tab[index] = loHead;
//判断高位有没有节点,没有的话相当于扩容前数组该下标的树全是低位树,则不需要平衡
if (hiHead != null) // (else is already treeified)
//有元素则低位树还需要平衡出根节点和旋转
loHead.treeify(tab);
}
}
//高位放入新数组原下标+老数组容量 下标处
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
3.8 untreeify
树变为链表时触发(树上节点<=6);
//传入map对象
final Node<K,V> untreeify(HashMap<K,V> map) {
//hd存放头节点,t1临时代替hd
Node<K,V> hd = null, tl = null;
//q存放当前树节点,因为Node是TreeNode父类,所以可以用父类引用子类
for (Node<K,V> q = this; q != null; q = q.next) {
//将当前树节点转为节点,其中next属性为null
//p存放当前节点
Node<K,V> p = map.replacementNode(q, null);
//判断是否是头节点(第一个节点)
if (tl == null)
hd = p;
else
//从头节点的next属性开始一直到最后
tl.next = p;
//t1临时代替头节点连接后面所有节点
tl = p;
}
//返回头节点,因为通过t1将链表已经与hd连接起来了
return hd;
}
3.9 treeify
将数组中调用该方法的对象(链表)改为树形结构
final void treeify(Node<K,V>[] tab) {
//root表示根节点
TreeNode<K,V> root = null;
//x存放当前节点,next存放下一个节点,遍历链表
for (TreeNode<K,V> x = this, next; x != null; x = next) {
//强转下一个节点为树形并赋给next
next = (TreeNode<K,V>)x.next;
//将当前节点的left,right定为空
x.left = x.right = null;
//判断有没有根
if (root == null) {
没有的话将当前节点置为根节点
x.parent = null;
x.red = false;
root = x;
}
//若有根就进入else
else {
//k,h存放当前遍历的节点的key和hash值
K k = x.key;
int h = x.hash;
Class<?> kc = null;
//p存放根
for (TreeNode<K,V> p = root;;) {
//dir用来判断当前节点该放在根的左边还是右边,比根小放左,大则放右
int dir, ph;
K pk = p.key;
//判断根节点hash是否比当前的节点hash大
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
四、remove
根据指定key删除节点
public V remove(Object key) {
//e中存放要删除的节点
Node<K,V> e;
//删除成功则返回删除节点的value属性值
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
4.1 removeNode
matchValue: 如果为真,则仅在值相等时删除 movable: 如果为 false,则在移除时不移动其他节点
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//tab存放数组,p存放当前节点(数组中index下标中的),n存放数组长度,index存放当前下标,
Node<K,V>[] tab; Node<K,V> p; int n, index;
//判断数组是否为null或者当前下标是否有节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node存放要删除的节点,e存放下一节点
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)
//使用getTreeNode获取
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//链表结构则循环遍历判断每个节点是否与删除节点相同,相同则赋值给node并跳出循环
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
//p一直存放被遍历节点的上一节点
p = e;
} while ((e = e.next) != null);
}
}
//判断是否存在要删除的节点,删除节点的value的指向地址同一个,是否为空,是否值相等
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//判断是不是树节点,是则调用removeTreeNode删除节点
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//判断是否为第一个节点相同
else if (node == p)
//将null赋值给当前下标的第一个节点
tab[index] = node.next;
else
//若不是树且也不是第一个节点,则直接将删除节点的next属性赋值给删除节点的上一节点的next属性
p.next = node.next;
++modCount;
//元素个数-1
--size;
//该方法为空,在LinkedHashMap中重写该方法
afterNodeRemoval(node);
//返回删除的node
return node;
}
}
//若没有要删除的节点,则返回null
return null;
}
五、修改
因为hashMap中采用key-value方式存放数据,其中key是唯一的,若key重复则会直接覆盖之前设置的value值,即修改。
//put中的部分源码
//判断是否存在相同的节点,有相同则进入
if (e != null) { // existing mapping for key
//记录原有的value值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//更新value值
e.value = value;
//空方法,该方法用于LinkedHashMap中
afterNodeAccess(e);
//返回原来的value
return oldValue;
}
六、get
获取查询key的value值
public V get(Object key) {
Node<K,V> e;
//e中存放数组中查询到key相等的节点,有则返回查询到的节点value值,没有则返回null
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
6.1 getNode
final Node<K,V> getNode(int hash, Object key) {
//tab存放数组,first存下标第一个节点,e存放当前节点,n存放数组长度,k存放key值
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//判断数组是否为null或者根据传入hash计算出来下标中没有元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//判断下标中第一个节点是否为要查询的节点key值,是则直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//判断是否只有一个节点
if ((e = first.next) != null) {
//若不止一个节点
//判断是否为树结构,是则调用getTreeNode方法查询树节点并返回结果
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//若为链表则直接遍历,然后一个个判断key是否相同,有则直接返回节点地址
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//没有要查询的key值则直接返回null
return null;
}
七、size
返回元素(节点)总个数
public int size() {
return size;
}
八、clear
清空数组,删除所有节点
public void clear() {
Node<K,V>[] tab;
//修改次数+1
modCount++;
//判断数组是否为null&&有没有元素节点
if ((tab = table) != null && size > 0) {
//元素置为0
size = 0;
//遍历数组,且将所有下标置为null(将下标的指向的地址置为null就相当于删除,因为那些节点没有地址引用了就会被GC垃圾回收)
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}