HashMap
**核心属性分析:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 填充因子,大概意思就是留出0.25给新数据
final float loadFactor; //默认值为0.75
}
1.构造
1.1HashMap()
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted //填充因子默认值为0.75
} //默认初始容量是16,但要到插入时才会创建数组
1.2HashMap(int)
直接得到初始容量,还是会给默认的填充因子
1.3HashMap(int,float)
校验两个数的合理性
0<initialCapacity<=2^30
loadFactor>0
最需要注意的还是下面转化为2的次方数
this.threshold = tableSizeFor(initialCapacity);
//转换得到2的次方数(返回一个>=当前initialCapacity的一个数)
1.4HashMap(Map<? extends K, ? extends V>)
2.增
public V put(K key, V value) {
return this.putVal(hash(key), key, value, false, true); //调用putVal
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab:引用当前hashMap的散列表
//p:表示当前散列表的元素
//n:表示散列表数组的长度
//i:表示路由寻址结果
//延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中最耗费内存的散列表
java.util.HashMap.Node<K,V>[] tab; java.util.HashMap.Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//寻址找到的桶位,刚好是null,这个时候,直接将当前k、v对应的node放进去
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//e:不为null的话,找到了一个与当前要插入的key-value一致的key的元素
//k:临时的一个key
java.util.HashMap.Node<K,V> e; K k;
//表示桶位中的该元素,与你当前插入的元素的key完全一致,后续需要进行替换操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof java.util.HashMap.TreeNode)
e = ((java.util.HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//链表的情况,而且链表的头元素与我们要插入的key不一致
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null); //插到链表末尾(9)
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;
if (++size > threshold) //size自增,大于扩容临界值时,触发扩容
resize(); //扩容
afterNodeInsertion(evict);
return null;
}
辅助分析图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J0lL1zQ9-1617369220159)(C:\Users\大懒\Desktop\Java学习内容·\集合分析\putVal.PNG)]
3.删
final HashMap.Node<K, V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
HashMap.Node[] tab; //引用当前hashMap中的散列表
HashMap.Node p; //当前的node元素
int n; //表示散列表数组长度
int index; //表示寻址结果
if ((tab = this.table) != null && (n = tab.length) > 0 && (p = tab[index = n - 1 & hash]) != null) {
//说明桶位有数据,需要进行查找操作并且删除
HashMap.Node<K, V> node = null; //查找到的结果
Object k;
//第一种情况,当前桶位中的元素就是要删除的元素
if (p.hash == hash && ((k = p.key) == key || key != null && key.equals(k))) {
node = p;
}
else {
HashMap.Node e;
if ((e = p.next) != null) {//说明当前桶位要么是链表要么是红黑树
if (p instanceof HashMap.TreeNode) { //判断当前桶位是否升级位红黑树
node = ((HashMap.TreeNode)p).getTreeNode(hash, key); //第二种情况:进行红黑树查找操作
}
else { //第三种情况,链表
label88: {
while(e.hash != hash || (k = e.key) != key && (key == null || !key.equals(k))) {
p = e; //循环查找链表上的键值
if ((e = e.next) == null) {
break label88;
}
}
node = e;
}
}
}
}
//下面为删除逻辑
Object v;
//判断node不为空的话,说明按照key查找到需要删除的数据
if (node != null && (!matchValue || (v = ((HashMap.Node)node).value) == value || value != null && value.equals(v))) {
//第一种情况:node是树节点,说明需要进行树节点移除操作
if (node instanceof HashMap.TreeNode) {
((HashMap.TreeNode)node).removeTreeNode(this, tab, movable);
}
//第二种情况:桶位元素即为查找结果,则将元素下一个元素放入桶中
else if (node == p) {
tab[index] = ((HashMap.Node)node).next;
}
else { //第三种情况:将当前元素p的下一个元素设置成 要删除元素的 下一个元素
p.next = ((HashMap.Node)node).next;
}
++this.modCount;
--this.size;
this.afterNodeRemoval((HashMap.Node)node);
return (HashMap.Node)node;
}
}
return null;
}
4.改
5.查
1.先判断桶中有没有数据,没有就直接退出
2.有数据:第一种情况:正好定位的桶位元素和想查找的数据一致
第二种情况:桶位不止一个元素,可能是链表也可能是红黑树
3.桶位升级为了红黑树,红黑树查找
桶位形成链表,链表查询得到相同的键位、hash值;返回元素,否则就循环查询直至结束
6.扩容
HashMap的容量变化通常存在以下几种情况:
-
空参数的构造函数:实例化的HashMap默认内部数组是null,即没有实例化。第一次调用put方法时,则会开始第一次初始化扩容,长度为16。
newCap = 16; newThr = 12; //初始化扩容
-
有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的2的幂数,将这个数设置赋值给阈值(threshold)。第一次调用put方法时,会将阈值赋值给容量 。
newCap = oldThr;
-
如果不是第一次扩容,则容量和阈值都变为原来的2倍时,负载因子还是不变
//扩容之前的table数组大小已经达到最大阈值后,不进行扩容,将扩容阈值设置为int最大值 if (oldCap >= 1073741824) { this.threshold = 2147483647; return oldTab; } //oldCap左移一位实现数值翻倍,并且赋值给newCap,newCap小于数组最大值限制,并且扩容之前的阈值大于等于16 //此时进行下一次扩容的阈值等于当前阈值翻倍 if ((newCap = oldCap << 1) < 1073741824 && oldCap >= 16) { newThr = oldThr << 1; }
注意!!!
- 首次put时,先会触发扩容(算是初始化),然后存入数据,然后判断是否需要扩容;
- 不是首次put,则不再初始化,直接存入数据,然后判断是否需要扩容;
采用什么数据结构?
数组+链表+红黑树实现
如何实现的?为什么这么实现?
线程是否安全?
说一下工作原理?
说一下每个函数的执行过程?
与其他数据结构的区别?
这几天写的集合源码总结,希望对有需要的人有点用