源码系列
目录
一.什么是HashMap?
二、HashMap特点
1.底层结构
在jdk1.8之前使用 数组+链表
在jdk1.8,使用 数组+链表/红黑树,在链表长度达到8时会转换成红黑树
为什么要使用红黑树?
在极端情况下,可能一条链表会很长,那么此时进行查找操作就会变得非常慢。而红黑树可以很大程度降低查找时间。
不懂得红黑树的同学可以先去看一下红黑树相关概念,不然在红黑树的源码部分会非常难以理解。
2.索引的计算
hash(key)&(len-1):键的hash值与(tab长度-1)的位计算
3.哈希冲突
在tab中,由于长度有限,难免会存在hash(key)&(n-1)相同的情况,这样就出现了冲突的问题。哈希冲突不能避免,只能尽量降低冲突的概率。
hashmap中是如何降低hash冲突概率的?请继续往下看源码
hashmap中出现hash&(n-1)相同时怎么办?
拉链法:将链表和数组相结合,创建一个链表数组,数组每一个就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
4.为什么扩容是2的幂次方
因为(n - 1) & hash。大家都知道,自己实现hashmap的时候,是用hash%n来计算索引的位置,而hashmap中却是使用&运算来做的,这是因为在计算机中,&运算相当高效,就是直接对整数在内存中的二进制位进行操作。按位与&的计算方法是,只有当对应位置的数据都为1时,运算结果也为1,当HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。
例如:
上面对比可以看出,非2的幂次方hash冲突概率增大了!
5.为什么hashmap可以存储null,而hashtable不可以?
在hashmap中使用索引位置为0的位置存储null值。
而在hashtable中,因为使用的是安全失败机制,该机制会使读到的数据不一定是最新的数据。如果使用null,就会使其无法判断key是存在还是为空,因为无法再调用一次containsKey(key)来对key是否存在进行判断
6.快速失败与安全失败
快速失败:在遍历过程中,会使用一个modCount来记录这个map中发生变化的次数,如果在遍历时发生了变化,就会改变modCount的值,在迭代器使用hasNext()/next()方法之前,会看一下modCount是否发生了变化,如果发生了变化,就会抛出异常。
安全失败:在遍历时,不是直接在集合内容中访问的,而是复制原有内容,在原有内容上进行访问。在遍历过程中,对原集合所作的修改并不能被迭代器检测到,不会抛出异常。
7.线程安全
在jdk1.8之前使用了头插法:新来的值取代原来的值的位置,而原来的值被顺推到链表中(因为作者认为后来的值被查找的可能性更大)
使用头插法出现的问题:在多线程操作时可能会引起死循环的问题,因为扩容前后链表顺序倒置,在转移过程中修改了原来链表中的节点关系。
在jdk1.8使用了尾插法:将新来的值增加到后面
为什么使用尾插法?
1、如果不使用尾插法,多线程时可能会出现死循环的问题
2、在jdk1.8后,链表长度大于8会变成红黑树,使用头插法会改变链表上的顺序,但如果使用尾插法,在扩容时,保证元素原本的顺序,并且不会出现链表成环的问题。
但是你以为这样就解决了多线程不安全的问题吗?当然不是,使用尾插法会出现数据丢失的问题。
尾插法的问题:在多线程扩容情况下,如果有两个线程:线程1,线程2,进行put操作,且hash函数计算出插入下标相同。当线程1执行完计算hash值,线程给到线程2,在该下标处插入元素,完成正常的插入操作。此时线程给到线程1,由于之前已经计算过hash值,会出现hash碰撞,线程1并不知道线程2已经插入了元素,故线程1的元素会覆盖掉线程2的元素。
8.hashmap到底是先扩容再加载还是先加载再扩容?
(1)hashmap首次插入数据是先扩容后插入数据
(2)首次之后是先插入数据,后进行扩容
详细原因请继续往下看源码
二、源码分析
1.常量
//序列号
private static final long serialVersionUID = 362498820763181265L;
/**
* 默认大小,1左移四位,10000->16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量:2^30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 负载因子0.75
*
* 太大?
* 越大,则hash表中填的元素越多,hash冲突概率增大,每个元素下面的链表会太长,查找效率降低
*
* 太小?
* 越小,则填的元素越少,空间利用率降低,hash冲突降低,但是数组的元素过于稀疏,很多空间还没用就开始扩容
*
* 0.75是一个折中的方案
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 链表->红黑树的转换阈值=8,超过时,从链表转为红黑树
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 当低于这个值的时候,树转换成链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 位桶(bin)处的数据要采用红黑树结构进行存储时,整个table的最小容量
*/
static final int MIN_TREEIFY_CAPACITY = 64;
2.构造方法
/**
* 构造方法
*
* @param initialCapacity 初始大小
* @param loadFactor 加载因子
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果超过了最大容量值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//如果加载因子为null或者小于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//调用tableSizeFor,找到最接近initialCapacity的2的幂次方
this.threshold = tableSizeFor(initialCapacity);
}
/**
* 返回根据给定的目标容量所计算出来的最接近2的幂,主要目的是为了均匀分布
* 这里保证了容量是2的幂次方
*/
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;
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
3.Hash方法
/**
* hash值计算 如果key为空,就放入索引为0的同中,否则h>>>16右移16位
* 为什么要右移16位,并且是异或运算?
* 目的是降低hash冲突的概率,int类型的数值是4个字节的,右移16位异或可以同时保留高16位于低16位的特征
*
* 首先将高16位无符号右移16位与低十六位做异或运算。
* 如果不这样做,而是直接做&运算那么高十六位所代表的部分特征就可能被丢失
* 因为 index=hash&(n-1) 如果n比较小的话,那么高十六位的特征就被丢失了
* 将高十六位无符号右移之后与低十六位做异或运算使得高十六位的特征与低十六位的特征进行了混合得到的新的数值中就高位与低位的信息都被保留了 ,
* 而在这里采用异或运算而不采用& ,| 运算的原因是 异或运算能更好的保留各部分的特征,
* 如果采用&运算计算出来的值会向1靠拢,采用|运算计算出来的值会向0靠拢
*
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
重点理解h为什么要右移16位?看注释!!!自己可以手动计算一下。
4.getNode
/**
*通过获取key的hash值来定位
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first, e;
int n;
K k;
//对table进行校验:table不为空,size>0,(n-1)&hash计算索引值,索引位置的节点不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//看first的hash值是否与要目标的hash值是否相同,key是否相同,如果相同,直接返回该节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//e为first的下一个节点,first后面有节点的话
if ((e = first.next) != null) {
//看是不是树,是树的话调用红黑树的查找方式
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//遍历链表进行查找,知道hash和key和目标节点相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//如果没有找到就返回null
return null;
}
/**
* 调用二叉树查找方式
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
/**
* 红黑树节点的查找方法,特征:左节点的值<根节点的值<右节点的值
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
//令p为根节点
TreeNode<K,V> p = this;
do {
int ph, dir;
K pk;
//pl为左子节点,pr为右子节点
TreeNode<K,V> pl = p.left, pr = p.right, q;
//如果传入的节点的hash值小于根节点的hash值,向左遍历
if ((ph = p.hash) > h)
p = pl;
//如果传入的节点的hash值大于根节点的hash值,向左遍历
else if (ph < h)
p = pr;
//如果根节点就是要找的节点,返回该节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//如果左子树为空,令p为右子树
else if (pl == null)
p = pr;
//如果右子树为空,令p为左子树
else if (pr == null)
p = pl;
//将p节点与k进行比较
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
//k<pk向左遍历,否则向右遍历
p = (dir < 0) ? pl : pr;
//对右子树进行遍历,如果不为空,表示找到了节点,直接返回
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
//表示右子树没找到目标节点,向左进行遍历
p = pl;
} while (p != null);
return null;
}
5.put(篇幅较长,核心方法在红黑树前的部分)
/**
* 往hashMap中加入元素
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//如果table为空,则进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果目标元素要放置的位置为空,直接在该索引位置上新增一个节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e;
K k;
//判断节点p的hash,key值是不是与加入的元素的hash,key值是否相同,如果相同,则将p节点赋值给e节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断p节点是否为TreeNode,如果是调用红黑树的putTreeVal方法查找目标
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//为链表节点,使用binCount统计节点个数,如果超过了8转为红黑树
for (int binCount = 0; ; ++binCount) {
//没有找到相同的节点,在最末尾添加该节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//判断是否超过了8个
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//判断是否与新增的节点相同,相同则跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//指向下一个节点
p = e;
}
}
//如果e节点不为空,说明要新加的节点存在,则将value覆盖节点的value
if (e != null) {
// existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果超过了阈值,则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/**
* 将链表转成红黑树
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index;
Node<K,V> e;
//如果table为空或者table的长度小于64,调用resize方法进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//根据hash值计算出索引值,将该索引位置的节点赋值给e,从e开始遍历该索引位置的链表
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//将链表节点转成红黑树节点
TreeNode<K,V> p = replacementTreeNode(e, null);
//如果是第一次遍历,将头节点赋值给hd
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
//将p节点赋给tl,用于下一次循环中作为上一个节点进行链表的关联操作
tl = p;
} while ((e = e.next) != null);
//将table该索引位置赋值给新转的TreeNode的头节点,如果该节点不为空,则以头节点hd 为根节点,构建红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
/**
* 构建红黑树
*/
final void treeify(Node<K,V>[] tab) {
//根节点
TreeNode<K,V> root = null;
//将调用该方法的节点赋值给x,以x为起点,开始进行遍历
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
//如果还没有根节点,将x设置为根节点
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
//如果当前节点x不是根节点,则从根节点开始查找属于该节点的位置
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
//如果x的hash值小于p的hash值,将dir=-1,表示向p左边进行查找
if ((ph = p.hash) > h)
dir = -1;
//如果x的hash值大于p的hash值,将dir=1,表示向p右边进行查找
else if (ph < h)
dir = 1;
//如果k没有实现Comparable接口
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
//使用定义的规则来比较x和p节点的大小,来决定向左还是向右
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;
}
}
}
}
//如果root节点不在table索引位置的头节点,将其调整为头节点
moveRootToFront(tab, root);
}
/**
* 将root节点放到头节点的位置
* 如果当前索引位置的头节点不是root节点,则将root的上一个节点和下一个节点进行关联
* 将root放到头节点位置,原头节点放在root的next节点上
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
//判断root、tab、tab的长度 是否为空
if (root != null && tab != null && (n = tab.length) > 0) {
//索引位置的计算
int index = (n - 1) & root.hash;
//令first节点设置为tab的首节点
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
//如果root节点不是首节点
if (root != first) {
Node<K,V> rn;
//将root设置为首节点
tab[index] = root;
//rp赋值为root的上一个节点
TreeNode<K,V> rp = root.prev;
//移除root的过程
//将rp插入到root和root.next中
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
//如果原始首节点不为空,将它的前一个节点设置为root
if (first != null)
first.prev = root;
root.next = first;
//此时root 已经被放到该index位置的头节点位置,因此将设为空
root.prev = null;
}
//检查树是否正常
assert checkInvariants(root);
}
}
/**
*红黑树的put方法,红黑树插入会同时维护原来的链表属性,即原来的next属性
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
//查找根节点,索引位置的头节点不一定为红黑树的根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
//将根节点赋值给p节点,开始进行查找
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk;
//如果传入的hash值小于根节点Hash值,将dir=-1,则左做查找
if ((ph = p.hash) > h)
dir = -1;
//如果传入的Hash值大于根节点hash值,将dir=-1,向右查找
else if (ph < h)
dir = 1;
//如果p的hash/key值与传入的hash/key值相等,则返回p
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//如果k所属的类没有实现Comparable接口,或者k和p节点的key相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//如果没有找到
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
//分别向左节点和右节点进行查找
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
//否则使用定义的一套规则来比较k和p节点的key的大小,来决定向左还是向右查找
//dir<0表示k<pk ,则向p的左边进行查找。否则向右边
dir = tieBreakOrder(k, pk);
}
//xp赋值为x的父节点,中间变量,用于下面给x的父节点赋值
TreeNode<K,V> xp = p;
//如果dir<=0向左进行查找,否则向右进行查找,如果位null,则表示该位置为x的目标位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//走进来表示找到x的位置,只需将x放入该位置即可
Node<K,V> xpn = xp.next;
//将x节点插入xpn与xp之间
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//如果dir<=0,表示x节点为xp的左节点。否则为右节点
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
//进行红黑树的插入平衡调整
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
流程图只描述了个整体的过程,里面还有很多细节的东西,需要到源码中查看。
6.reSize(索引位置的计算最好自己动手计算一下会好理解)
/**
* 扩容操作
* hashmap何时会扩容?
*(1)到达阈值:map当前容量*负载因子
*(2)当使用红黑树结构存储的时候,tab长度小于64
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//如果老表的容量大于最大容量值,就把扩容阈值设置为Integer的最大值,并直接返回老表
//此时oldCap*2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯的将阈值扩容到最大
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//newCap=2*oldCap<MAXIMUM_CAPACITY,并且oldCap>=16,将新阈值设置为原来的两倍
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;
else {
//老表的容量为0,老表的阈值为0,这种情况是没有传初始容量的new方法创建的空表,将阈值和容量设为默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新表的阈值为0,则通过新的 容量*负载因子 获取阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表
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;
//将索引值为j的老表头节点赋给e
if ((e = oldTab[j]) != null) {
//将老表的节点设置为空,便于垃圾回收
oldTab[j] = null;
//如果头节点的下一个节点为空,则重新计算索引值,将e设置为新表中的新索引位置的值
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果e是二叉树,则进行红黑树的hash分布(与链表的hash分布基本相同)
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// preserve order
//e是链表
//存储位置为 原索引位置 的节点
Node<K,V> loHead = null, loTail = null;
//存储位置为 原索引位置+oldCap 的节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//如果e的hash值与老表容量的与运算为0,则扩容后的索引位置跟老表的索引位置一样
//e.hash&oldCap解析:可以通过hash&(n-1)推算得出,其索引位置与扩容操作无关,
// 因为扩容之后n-1的高位为1,此时hash值只能补上0跟他做&计算,仍然为0,故没有关系
if ((e.hash & oldCap) == 0) {
//如果loTail为空,表示该节点为第一个节点
if (loTail == null)
//将loHead赋值给第一个节点
loHead = e;
else
//否则将节点添加到loTail后面
loTail.next = e;
//将loTail设置为新增的节点
loTail = e;
}
//如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
//扩容的索引位置计算:扩容前:hash&(n-1) 扩容后:hash&(2n-1)=老表的索引位置+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//如果loTail不为空,说明老表的数据有分布到新表 原索引位置 的节点,
//则将最后一个节点的下一个节点置空,并将新表上索引位置的 源索引位置 的节点设置为对应的头节点
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//如果hiTail不为空,说明老表的数据有分布到新表上 原索引+oldCap位置 的节点
//将最后一个节点的下一个节点设为空,并将新表上索引位置为 原索引+oldCap 节点设置为对应的头节点
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回新表
return newTab;
}
reSize部分主要分为两部分:
(1)计算扩容长度与阈值,建立新表
(2)数据的迁移过程
7.remove
/**
*移除某个节点
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
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;
//判断tab是不是为空,如果不为空并且长度大于0,通过hash&(n-1)索引位置上的节点赋给p
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节点就是要找的节点,将node=p
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//如果不是,则将p的下一个节点赋给e
else if ((e = p.next) != null) {
//判断是不是红黑树
if (p instanceof TreeNode)
//按照红黑树的方法查找节点
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//是链表,进行遍历查找目标节点
do {
//当前节点的hash值和key值与目标节点的一致
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != 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)
//如果是索引位置上的节点,则将该索引位置节点设置为node的下一个节点
tab[index] = node.next;
else
//不是头节点,将node的上一个节点的next属性设置为node的next节点
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
//返回被移除的节点
return node;
}
}
return null;
}
/**
*红黑树节点移除
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
//tab为空或者tab长度为0直接返回
if (tab == null || (n = tab.length) == 0)
return;
//计算索引位置
int index = (n - 1) & hash;
//将tab[index]首节点赋给first和root
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
//该方法被将要被移除的Node(TreeNode)调用,因此此方法的this为要被移除node节点
//将node的next节点赋值给succ节点,prev节点赋值给pred节点
//succ=node.next
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
//如果pred节点为空,表示要被移除的node节点为头节点
//将tab索引位置的节点和first设置为node.next
if (pred == null)
tab[index] = first = succ;
else
//否则将pred节点的next属性设置为node.next
pred.next = succ;
//如果succ不为空,将succ的prev节点设置为pred
if (succ != null)
succ.prev = pred;
//如果此时first节点为空,表示该索引位置已经没有目标节点,直接返回
if (first == null)
return;
//如果root的父节点不为空,则将root赋值为根节点
if (root.parent != null)
root = root.root();
//如果此时root为空,或者左右子树都为空,则判断红黑树是否太小
// 如果是,则调用unterrify方法转为链表节点并返回
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
//将p赋值为要被移除的节点,pl为p的左节点,pr为p的右节点
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
//如果左右子树都不为空
if (pl != null && pr != null) {
//将s赋值为pr(p的右子树)
TreeNode<K,V> s = pr, sl;
//一直向左找,跳出循环时,s为没有左节点的节点
while ((sl = s.left) != null) // find successor
s = sl;
//交换p和s的颜色
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
//第一次调整和第二次调整:将p节点和s节点进行位置交换
//第一次调整:
//如果p的右节点和就是s节点,则将p的父节点赋值为s,将s的右节点赋值为p
if (s == pr) {
// p was s's direct parent
p.parent = s;
s.right = p;
}
else {
//将sp赋值为s的父节点
TreeNode<K,V> sp = s.parent;
//把p的父节点设为sp
if ((p.parent = sp) != null) {
//如果s是sp的左节点,则把sp的左节点设为p
if (s == sp.left)
sp.left = p;
else
//否则把sp的右节点设为p
sp.right = p;
}
//将p的右节点设为s的右节点,不为空的话
if ((s.right = pr) != null)
//把p的右节点的父节点设为s
pr.parent = s;
}
//第二次调整:
//将p的左节点置空
p.left = null;
//把p的右节点设为s的右节点,并且不为空的话,将s的右节点的父节点设为p
if ((p.right = sr) != null)
sr.parent = p;
//把s的左节点设为p的左节点,并且不为空的话,把p的左节点的父节点设为s
if ((s.left = pl) != null)
pl.parent = s;
//把s的父节点设置为p的前一个节点,p的父节点为空的话
//则p节点为root节点,交换后s成为新的root节点
if ((s.parent = pp) == null)
root = s;
//如果p不是root节点,并且p是p的父节点的左节点,将p的父节点的左节点设为s
else if (p == pp.left)
pp.left = s;
else
//否则,将p的父节点的右节点设为s
pp.right = s;
//如果s的右节点不为空的话,则replacement为s的右节点
if (sr != null)
replacement = sr;
else
//如果s的右节点为空,则s为叶子节点,replacement为p本身只需要将p节点直接去除即可
replacement = p;
}
//如果p的左节点不为空,右节点为空,则replacement赋值为p的左节点
else if (pl != null)
replacement = pl;
//p的左节点为空,右节点不为空,则replacement赋值为p的右节点
else if (pr != null)
replacement = pr;
//p的左右节点都为空,replacement设置为p
else
replacement = p;
//第三次调整:使用replacement节点替换掉p节点的位置,将p节点移除
//如果replacement不是p的话
if (replacement != p) {
//将p的父节点设置为replement的父节点,同时赋给pp节点
TreeNode<K,V> pp = replacement.parent = p.parent;
//如果p没有父节点,则p就是root节点,将root赋值为replacement节点
if (pp == null)
root = replacement;
//如果p是其父节点的左节点,则将p父节点的左节点赋值为replacement
else if (p == pp.left)
pp.left = replacement;
//如果p是其父节点的右节点,将p父节点的右节点赋值为replacement
else
pp.right = replacement;
//p已经被替换掉了,将p节点清空,便于垃圾回收
p.left = p.right = p.parent = null;
}
//如果p节点不为红色则进行红黑树删除平衡调整
//如果删除的节点是红色则不会破坏红黑树的平衡,无需调整
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
//如果p节点为叶子节点,则直接p去掉即可
if (replacement == p) {
// detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
//如果p节点是其父节点的左节点
if (p == pp.left)
//将其左节点置为空
pp.left = null;
//如果是其父节点的右节点
else if (p == pp.right)
//将右节点置空
pp.right = null;
}
}
if (movable)
//将root节点移到索引位置的头节点
moveRootToFront(tab, r);
}
此处红黑树的操作较多,比较难理解,需要读者提前搞懂红黑树知识。
总结
其实,整个下来会发现hashmap的增删改查操作还是比较有规律的可寻的
但是源码的红黑树部分还是需要好好搞懂红黑树的知识才能更好地理解它!!!