HashMap数据结构
hashMap的概念
HashMap 是一个利用散列表(哈希表)原理来存储元素的集合,是根据Key value而直接进行访问的数
据结构
在 JDK1.7 中,HashMap 是由 数组+链表构成的。
在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成
hashMap变化历程
jdk1.7数据结构:基于链表+数组,总所周知链表时间复杂度为O(n),当链表过长每个节点进行equals判断很消耗性能
jdk1.8数据结构:基于链表+红黑树+数组,基于红黑树的添加查询性能(大的放右边,小的放左边,相等的不存)一定程度上形成了有序,进行了查询业务赋能大大优化
链表和红黑树相互转化的临界点条件:链表长度大于8(红黑树结点<6)&&数组长度大于64 两者缺一不可,才会发生树化反应,否则只会扩容操作。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); //缺少一个进行扩容
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
HashMap深度源码探究
目标:
通过阅读HashMap源码,我们可以知道以下几个问题在源码是如何解决的?
(1)HashMap的底层数据结构是什么?
(2)HashMap中put操作的底部实现原理是什么?
(3)HashMap是如何解决hash冲突的?(寻址计算+hash运算流程)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //16,默认容量:左位移4位,即
//2的4次幂
static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量:即2的30次幂
static final float DEFAULT_LOAD_FACTOR = 0.75f;//负载因子:扩容使用,统计学计算出
的最合理的
static final int TREEIFY_THRESHOLD = 8;//链表转红黑树阈值
static final int UNTREEIFY_THRESHOLD = 6;//当链表的值小<6, 红黑树转链表
..略
transient Node<K,V>[] table;//HashMap中的数组,中间状态数据;不参与序列化(重点)
transient Set<Map.Entry<K,V>> entrySet;//用来存放缓存,中间状态数据;
transient int size;//size为HashMap中K-V的实时数量(重点),中间状态数据;
transient int modCount;//用来记录HashMap的修改次数
int threshold;//扩容临界点;(capacity * loadFactor)(重点)
final float loadFactor;//负载因子 DEFAULT_LOAD_FACTOR = 0.75f赋值
这里TREEIFY_THRESHOLD = 8和UNTREEIFY_THRESHOLD = 6和DEFAULT_LOAD_FACTOR = 0.75f都是基于泊松分布实现的。
泊松分布
泊松分布(法语:loi de Poisson,英语:Poisson distribution)又称Poisson分布、帕松分布、布瓦松分布、布阿松分布、普阿松分布、波以松分布、卜氏分布、帕松小数法则(Poisson law of small numbers),是一种统计与概率学里常见到的离散概率分布,由法国数学家西莫恩·德尼·泊松在1838年时发表。
泊松分布适合于描述单位时间内随机事件发生的次数的概率分布。如某一服务设施在一定时间内受到的服务请求的次数,电话交换机接到呼叫的次数、汽车站台的候客人数、机器出现的故障数、自然灾害发生的次数、DNA序列的变异数、放射性原子核的衰变数、激光的光子数分布等等。
泊松分布的概率质量函数为:
泊松分布的参数λ是单位时间(或单位面积)内随机事件的平均发生率。
HashMap的put方法(jdk1.8)
工作流程:
当我们进行put的操作的时候,先将你传入过来的key进行hash,(该过程可以分为3个步骤,下文会讲),得到一个索引值index,之后取得索引值会得到table[]数组的索引位置。(这里可以分为4种情况,下文会讲),根据相应的情况进行不同的put操作
hash工作流程
-
先根据object的hashCode的方法得到一个int类型的hash值,此时hash值有可能为负数,但总所周知,索引值肯定不能为负数,否则会出现数组越界异常
-
将得到的hashCode的值进行右移16位,并与其进行异或运算,主要是将高位也参与进运算,降低hash冲突
-
再将得到的hash值与(table.length-1) &运算,得到数组下标索引.
源码
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
............
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
从这也可以得出hashMap是可以key为null的集合
put方法可分为4种情况:
A.未产生hash碰撞
- 如果当前桶位没有元素,则没什么好说的,直接putValue即可
B.产生hash碰撞
-
如果当前桶位有元素,但没产生链化(当前桶位只有一个元素),进行equlas判断,为true则直接覆盖oldValue,这也是hashMap不能有重复的元素,如果为false,则将oldValue挂在newValue下方,形成链表
-
如果产生已经链化反应,跟1步骤差不多,不过多了判断是否会产生树化反应
-
如果当前p instanceof TreeNode,则进行红黑树的添加操作
源码实例:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//onlyIfAbsent:true不更改现有值;evict:false表
//示table为创建状态
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
初始化数组(or扩容)
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;
//如果与第一个元素(数组中的结点)的hash值相等,key相等,直接覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//判断是否为红黑树结点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
//遍历到链表最后 1.生成链表 2.已经是链表()
if ((e = p.next) == null) {
//生成链表
p.next = newNode(hash, key, value, null);
//判断是否满足树化条件
if (binCount >= TREEIFY_THRESHOLD - 1) // 从0开始
//树化操作,点进这方法还需要判断数组容量是否大于最大树化容量64,否则扩容
treeifyBin(tab, hash);
break;
}
//
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // // key已经存在
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value; //用新的value值去覆盖老的value值
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //用来记录HashMap的修改次数
if (++size > threshold) //map数量是否大于容量
resize();//如果size大于threshold,就需要进行扩容
afterNodeInsertion(evict);
return null;
}
为什么hashMap要进行2幂次方运算(寻址计算)
主要是为了控制数组不发生越界异常,比如数组默认长度为16(二进制=0001 0000),table.length-1为15(0000 1111),key为0101 0101,进行&运算之后
高位跟经过减一的高位保持一样,低位最多为1111 //0000 1111(高位 低位) 0-15的数组范围,所以这样的特殊数字只能是2的幂次方