1.HashMap中的关键属性
观察源码,可以看到
(1)负载因子:loadFactory
他的默认值是0.75,表示在扩容前,HashMap空间填满程度的边界
(2)threshold
他是记录HashMap所能够容纳的键值对的边界,计算规则:负载因子 乘以 数组的长度
(3)size
他是用来记录HashMap实际存在的键值对的数量
(4)modCount
他用来记录HashMap内部结构发生变化的次数
(5)DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
常量属性,HashMap默认的容量值,默认是16
2.HashMap存储结构:HashMap是采用:数组 + 链表 + 红黑树(JDK1.8)
HashMap的数组部分称作Hash桶,数组元素保持在一个table的属性中,当链表长度大于8时,链表的数据将会以红黑树的形式进行存储,当链表长度降到6时,以链表形式进行存储。
每个Node节点,保存了用来定位数组索引位置的hash值和key、value,以及链表指向的下一个Node节点 。
Node类是HashMap的内部类,实现Map.Entry接口,他的本质可以理解成一个键值对
3.HashMap的工作原理
当我们向HashMap中插入数据时,首先要确定Node在数据中的位置,如何确定Node的存储位置?
最开始的hashCode: 1111 1111 1111 1111 0100 1100 0000 1010
右移16位的hashCode:0000 0000 0000 0000 1111 1111 1111 1111
异或运算后的hash值: 1111 1111 1111 1111 1011 0011 1111 0101
实际就是 --求模取余法
h = hashCode % (table.length - 1)
用hash值和数组的长度减1,取模,最后得到数组的下标,这样可以保证数据下标不越界
或者——过位运算
h&(length-1)保证获取的index一定在数组范围内,举个例子,默认容量16,length-1=15,h=18,转换成二进制计算为
1 0 0 1 0
& 0 1 1 1 1
__________________
0 0 0 1 0 = 2
最终计算出的index=2。有些版本的对于此处的计算会使用 取模运算,也能保证index一定在数组范围内,不过位运算对计算机来说,性能更高一些(HashMap中有大量位运算)
所以最终存储位置的确定流程是这样的:
4.当对象的HashCode相同时
因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。
5.如果两个键的 HashCode 相同,你如何获取值对象?
当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
发散:
问题:Jdk1.7到Jdk1.8HashMap 发⽣了什么变化(底层)?
底层数据结构不同:
- 1.7中底层是
数组+链表
- 1.8中底层是
数组+链表+红⿊树
,加红⿊树的⽬的是提⾼HashMap插⼊和 查询整体效率
链表插入位置不同:
- 1.7中链表插⼊使⽤的是
头插法
- 1.8中链表插⼊使⽤的是
尾插法
,因为1.8中插⼊key和value时需要判断链表元素个数
,所以需要遍历链表统计链表元素个数,所以正好就直接使⽤尾插法
扩容策略不同:
- 1.7中是
只要不小于阈值就直接扩容2倍
- 1.8的扩容策略会更优化,键值对数量
大于阈值时会扩容
,且链表长度大于8时,转变为红黑树前会检测hash桶数量不足64时,也会进行扩容
,而且扩容过程中,发现有树节点不足6时,会降为链表
HashMap在初始化和put后容量和阈值的变化
private static void test() throws Exception {
//默认初始容量来创建一个HashMap
HashMap m = new HashMap();
//获取HashMap整个类
Class<?> mapType = m.getClass();
//获取指定属性,也可以调用getDeclaredFields()方法获取属性数组
Field threshold = mapType.getDeclaredField("threshold");
//将目标属性设置为可以访问
threshold.setAccessible(true);
//获取指定方法,因为HashMap没有容量这个属性,但是capacity方法会返回容量值
Method capacity = mapType.getDeclaredMethod("capacity");
//设置目标方法为可访问
capacity.setAccessible(true);
//打印刚初始化的HashMap的容量、阈值和元素数量
System.out.println("初始数据 - 容量:"+capacity.invoke(m)+" 阈值:"+threshold.get(m)+" 元素数量:"+m.size());
for (int i = 0;i<25;i++){
m.put(i,i);
//动态监测HashMap的容量、阈值和元素数量
System.out.println("容量:"+capacity.invoke(m)+" 阈值:"+threshold.get(m)+" 元素数量:"+m.size());
}
}
输出结果: