HashMap个人笔记
关键常量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
是数组的长度。默认初始化时的容量(16),必须为2的幂次方
static final int MAXIMUM_CAPACITY = 1 << 30;
最大容量,2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认负载因子,代表什么时候会进行扩容操作(当到达链表长度的???待补充)
static final int TREEIFY_THRESHOLD = 8;
树化阈值 1,当一个桶中的链表长度超过8时会将存储结构转化为红黑树(需要同时满足树化阈值1、2)
static final int UNTREEIFY_THRESHOLD = 6;
解树化阈值,
执行resize扩容操作时
,当桶中的树结构节点数小于6,会将树转化为链表
static final int MIN_TREEIFY_CAPACITY = 64;
最小红黑树容量,当Map里面的数量超过这个值时,表中的桶才能进行树形化 ,否则桶内元素太多时会扩容,而不是树形化 为了避免进行扩容、树形化选择的冲突
关键变量
int threshold;
阈值,当table == {}时,该值为初始容量(初始容量默认为16);当
table被填充了,也就是为table分配内存空间后,
threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要
参考threshold。tableSizeFor的赋值属性
final float loadFactor;
负载因子(不叫加载)
,代表了table的填充度有多少,默认是0.75加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。两重含义:
一是代表了当hashmap到达哪一个数据量的时候需要进行扩容操作
二是代表当前这个loadFactor值所对应的空间利用率有多少,越大代表利用率越高,但也越容易发生hash碰撞
transient Node<K,V>[] table;
table在JDK1.8中我们了解到HashMap是由数组加链表加红黑树来组成的结构其中table就是HashMap中的数组,jdk8之前数组类型是Entry<K,V>类型。从jdk1.8之后是Node<K,V>类型。只是换了个名字,都实现了一样的接口:Map.Entry<K,V>。负责存储键值对数据的。
transient int size;
size为HashMap中K-V的实时数量,不是数组table的长度
transient int modCount;
每次扩容和更改map结构的计数器
源码分析
构造函数
含义:通过调用
tableSizeFor
,将传入的初始容量转化为一个大于等于初始容量,且最接近初始容量的2的整次幂
,再将值赋给阈值threshold
。
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;
//分析点 1
this.threshold = tableSizeFor(initialCapacity);
}
- 分析:其实应该是
this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
才对,但是table
的初始化在第一次put的时候才初始化,threshold
又会被重新赋值。猜想是与resize
里面的第二个判断if (oldThr > 0) { newCap = oldThr; }
有关。
tableSizeFor
含义:根据传入的初始容量,返回一个
大于等于初始容量,且最接近初始容量的2的整次幂
作为真正的容量
static final int tableSizeFor(int cap) {
//分析点 1
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;//分析点 2
return (n < 0) ?
1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//分析点 3
}
int n = cap - 1;
分情况:
① 输入值刚好是2的幂次方:例如8(1000),如果不对它进行
-1
操作,得到的结果是16(10000),显然是错的② 输入值不是2的幂次方:取7或15,减不减1结果都是一样的
n |= n >>> 16;
- 再右移一位也就是乘以2,这时已经是2^32,已经是负数了
- 这个方法的目的是
生成2的幂次长度的容量
,不断右移可以让最高位后面的位全部变成1,最后再+1就可以保证一定是2的整次幂
n + 1
做完 ② 的移位操作后,最高位后面的所有位都会是
1
,再加1就可以得到一个2的整次幂
hash
对key的hashcode进行扰动,目的是让值更加随机,这样在
put(不一定只有put)
的时候取模(取余)就不会让所有的key聚集在一起,提高散列的均匀程度
static final int hash(Object key) {
int h;
//分析点 1
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
32位
的h进行右移16位,得到高16位为0,低16位为原值高16位的一串32位的数
,再将两个值做异或运算(相同为0,不同为1),
- 为什么高16位也参与?
因为hash & (n-1)
的结果只取决于低位,所以要的是低位,如果hashcode的1
集中在高位的话,在put方法中取数组下标的时候就有很大概率会是全0(例如对象 A 的 hashCode 为 1000010001110001000001111000000,对象 B 的 hashCode 为 0111011100111000101000010100000。数组长度为16(16-1=15(1111)),15与A、B做&运算后结果都是0,造成哈希冲突)
。所以是为了让低位的可能性更多,避免后续取模时key聚集。- 为什么右移16位、为什么hashcode返回32位
因为再右移就是16乘以2等于32了,超过整型范围,变成负数了;因为int占4个字节
get
参数列表:
- hash:对key进行hash扰动后的值
- key:键值
返回值列表:- Node<K,V>:节点
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[