HashMap
1.HashMap简介
HashMap集合:是基于哈希表的Map接口实现,是以key-value存储形式存在的(key,value都可以为null),主要用来存放键值对
HashMap的实现不是同步的,意味着它不是线程安全的和映射不是有序的
JDK1.8之前:HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突(两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引值相同)而存在的(拉链法解决冲突)
JDK1.8之后:在解决哈希冲突时有了较大的变化,当链表长度大于阀值(或者红黑树的边界值,默认为8)并且当前数组的长度大于64时,此索引位置上的所有数据改为使用红黑树存储
JDK版本 | 实现方式 | 节点数>=8 |
---|---|---|
1.8之前 | 数组+单向链表 | 数组+单向链表 |
1.8之后 | 数组+单向链表+红黑树 | 数组+红黑树 |
补充:将链表转换成红黑树前会判断,即使阀值大于8,但是数组长度小于64,并不会将链表变成红黑树,而是选择进行数组扩容
2.HashMap集合的数据结构存储过程
数据结构:存储数据的一种方式
面试题:
1.哈希表底层采用何种算法计算hash值?还有那些算法可以计算出hash值?
底层采用key的hashCode()方法的值结合数组长度进行无符号右移(>>>),按位异或(^),按位与(&)计算出索引
还可以采用:平方取中法,取余法,伪随机数法
2.当两个对象的hashCode值相等时会怎样
会产生哈希碰撞,若key值内容相同则替换旧的value,否则连接到链表后面,链表长度超过阀值8并且数组大于64就转换为红黑树存储
3.何时发生哈希碰撞和什么是哈希碰撞?如何解决哈希碰撞?
只要两个元素的key计算的哈希值相同就会发生哈希碰撞。JDK8前使用链表解决哈希碰撞;JDK8之后使用链表+红黑树解决哈希碰撞
如果两个键的hashcode相同,如何存储键值对?
hashcode相同,通过equals()方法比较内容是否相同
相同:新的value覆盖之前的value
不相同:将新的键值对添加到哈希表中
默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来
3.HashMap集合的成员变量
1.序列化版本号(serialVersionUID)
2.集合的初始化容量(DEFAULT_INITIAL_CAPACITY默认的初始化容量是16,必须是2的n次幂)
为什么必须是2的n次幂?如果输入值不是2的幂比如10会怎么样?
(1)当我们根据key的hash确定其在数组的位置时,如果n为2的幂次方,可以保证数据的均匀插入,如果n不是2的幂次方,可能数组的一些位置永远不会插入数据,浪费数组的空间,加大hash冲突
(2)HashMap容量为2次幂是为了数据的均匀分布,减少hash冲突,毕竟hash冲突越大,代表数组种一个链的长度越大,会降低hashmap的性能
(3)如果创建HashMap对象时,输入的数组的长度是10,不是2的幂,HashMap通过位移运算和或运算得到的肯定是2的幂次方,并且是离那个数字最近的幂方(例如10,会得到16)
HashMap如果创建集合时指定的容量不是2的n次幂的情况?
如果数组长度不是2的n次幂,计算出的索引特别容易相同,及其容易发生hash碰撞,导致其余数组空间很大程度上并没有存储数据,链表或者红黑树过长,效率降低
3.默认的负载因子,默认值是0.75(DEFAULT_LOAD_FACTOR)
4.集合最大容量(MAXIMUM_CAPACITY上限是:2的30次幂)
5.当链表的值超过8则会转红黑树(1.8之后新增)
为什么Map桶中节点个数超过8才转为红黑树?
推荐文章:https://www.cnblogs.com/linghu-java/p/10598758.html
6.当链表的值小于6则会从红黑树转回链表(UNTREEIFY_THRESHOLD)
7.桶中结构转化为红黑树对应的数组长度最小的值(MIN_TREEIFU_CAPACITY)
8.table用来初始化(必须是2的n次幂)
负载存储键值对数据的。JDK1.8之前数组类型是Entry<K,V>类型,JDK1.8之后是Node<K,V>类型,都是实现了Map.Entry<K,V>接口
9.HashMap存放元素的个数(size)
存放元素的个数,不等于数组的长度
10.用来记录HashMap的修改次数(modCount)
11.用来调整大小下一个容量的值计算方式(threshold=容量*负载因子)
12.哈希表的加载因子(loadFactor)
加载因子(loadFactor)是用来衡量HashMap的疏密程度,影响hash操作到同一个数组位置的概率
计算HashMap的实时加载因子的方法:size(数组实时存放元素)/capacity(桶的数量)
加载因子太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散,loadFactor的默认值为0.75f是官方给出的一个比较好的临界值
当hashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,而扩容这个过程设计到rehash,复制数据等操作,非常消耗性能。所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免
总结:既兼顾数组利用率又考虑链表不要太多,经过大量测试0.75是最佳方案
threshold计算公式:capacity(数组长度默认16)*loadFactor(负载因子默认0.75).这个值是当前已占用数组长度的最大值。衡量数组是否需要扩增的标准:当Size>=threshold的时候,要考虑对数组的resize(扩容),扩容后的HashMap容量是之前容量的两倍
4.HashMap集合的成员方法
put()方法推荐文章:https://blog.csdn.net/meng_lemon/article/details/88906980
treeifyBin()方法:用于链表转化为红黑树的方法(https://www.cnblogs.com/flydoging/p/10384362.html)
扩容机制
1.什么时候才需要扩容?
(1)当HashMap的元素个数超过数组大小(数组长度)*loadFactor(负载因子)时,就会进行扩容
(2)当HashMap中的其中一个链表的对象个数如果达到了8个,此时如果数组长度没有达到64,那么HashMap会先扩容解决,如果已经达到课了64,那么这个链表会变成红黑树,节点类型由Node变成TreeNode类型。当然,如果映射关系被移除后,下次执行resize()方法时判断树的节点个数低于6,也会在把树转换为链表
2.HashMap的扩容是什么
1.8之前:进行扩容会伴随一次重新hash分配,并且会历遍hash表中所有的元素,是非常耗时的(编写程序要尽量避免resize)
1.8之后:进行扩容时,使用的rehash方式非常巧妙,因为每次扩容都是翻倍,与原来计算的(n-1)&hash的结果相比,只是多了一个bit位,所以节点要么在原来的位置,要么就被分配到“原位置+旧容量”的位置
resize()方法:负责扩容的方法
remove()方法:删除元素的方法
get()方法:负责查找元素
5.历遍HashMap集合的四种方式
(1)分别遍历Key和Values
(2)使用Iterator迭代器迭代
(3)通过get方式(效率低不建议使用)
(4)JDK8之后使用Map接口中的默认方法
6.HashMap的初始化操作
如果知道由多少键值对需要存储,在初始化HashMap的时候指定它的容量,以防止HashMap自动扩容,影响使用效率
初始化容量设置成多少效率更高?
关于这个值的设置,在《阿里巴巴Java开发手则》有以下建议:
initialCapacity=(需要存储的元素个数 / 负载因子(默认值为0.75))+1
例如设置值是7,通过上面公式:7/0.75+1=10,10经过JDK处理之后,会被设置成16,这就大大的减少了扩容几率