一、Hashmap插入一个元素过程
1、先计算要插入元素的hashcode,再将这个hashcode无符号右移16位,再将右移过的hashcode和右移之前的hashcoe进行异或(相同为0,不相同为1)操作得到最终的hash值。(即高16位和低16位做异或操作,使hash值更分散)
2、将得到的hash值与hashmap的长度length-1做&操作,得到hash槽的位置。
3、 ①. 判断table是否为空,如果为空执行resize()进行扩容,初次扩容初始容量是16;
-
②. 根据键值key计算的hash值确定数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
-
③. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
-
④. 判断table[i] 是否为TreeNode,即table[i] 是否是红黑树,如果是红黑树,遍历发现该key不存在 则直接在树中插入键值对;遍历发现key已经存在直接覆盖value即可;
-
⑤. 如果table[i] 不是TreeNode则是链表节点,遍历发现该key不存在,则先添加在链表结尾, 判断链表长度是否大于8,大于8的话把链表转换为红黑树;遍历发现key已经存在直接覆盖value即可;
-
⑥. 插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过,进行扩容。
resize()扩容方法:
1、先判断hashmap的长度是否超过最大值(Integer.MAX_VALUE),超过最大值就不扩容了。
2、如果没超过,扩容为原来2倍。
3、将旧hashmap中的值重新计算后放到扩容后的hashmap中
Hash值的新增参与运算的位是1,扩容前的原始位置+原始容量的大小值。
(当前元素和旧的数组的长度做&操作,等于0新增参与运算的位是0,等于1新增参与运算的位是1)
hashcode计算详解https://www.cnblogs.com/mxxct/p/13857097.html
二、Hashmap1.7和1.8有哪些区别
1、JDK1.7使用的是数组+ 单链表的数据结构。而JDK1.8数组+链表+红黑树的数据结构。
当链表的深度达到8(默认阈值)并且总元素总个数超过树形化阈值(默认64),就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率,当树的节点小于6的时候转成链表。
当链表的深度达到8(默认阈值)并且元素总个数不超过树形化阈值(默认64),此时扩容。
红黑树的特性:
- 每个结点是黑色或者红色。
- 根结点是黑色。
- 每个叶子结点(NIL)是黑色。 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!]
- 如果一个结点是红色的,则它的子结点必须是黑色的。
- 每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!]
2、解决冲突时往链表中添加节点时,JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法。
原因:尾插法需要遍历整个链表,比较费时,所以JDK1.7用的是头插法。但是JDK1.8加入了红黑树,每次插入一个节点,需要判断链表的长度是否大于8,大于8转成红黑树,计算链表的长度需要遍历整个链表,所以既然遍历了链表,就直接插到尾部。
3、扩容后数据存储位置的计算方式也不一样(jdk8之HashMap resize方法详解(深入讲解为什么1.8中扩容后的元素新位置为原位置+原数组长度)_诺浅的博客-优快云博客_为什么新的位置是原位置+原数组长度)
JDK1.7:重新计算hash值放入到新的table中。
JDK1.8:Hash值的新增参与运算的位是0,扩容前的原始位置。
Hash值的新增参与运算的位是1,扩容前的原始位置+原始容量的大小值。
(当前元素和旧的数组的长度做&操作,等于0新增参与运算的位是0,等于1新增参与运算的位是1)
4、在JDK1.7的时候是先扩容后插入的,这样就会导致无论这一次插入是不是发生hash冲突都需要进行扩容,如果这次插入的并没有发生Hash冲突的话,那么就会造成一次无效扩容,但是在1.8的时候是先插入再扩容的,优点其实是因为为了减少这一次无效的扩容,原因就是如果这次插入没有发生Hash冲突的话,那么其实就不会造成扩容,但是在1.7的时候就会急造成扩容
5、JDK1.7用了9次扰动处理=4次位运算+5次异或,而JDK1.8只用了2次扰动处理=1次位运算+1次异或。
三、Hashmap在多线程下会出现什么问题
1、JDK1.7:在扩容时链表可能成环,导致查找的时候出现死循环。(因为JDK1.7是头插法,扩容的时候链表元素会倒置)
JDK1.8:不会出现链表成环的情况,因为JDK1.8是尾插法。
2、put操作时可能覆盖元素。
四、loadFactor的默认值为什么为0.75f
loadFactor装载因子用来衡量HashMap满的程度
- 若加载因子越大,填满的元素越多。好处是空间利用率高了。但是冲突的机会加大了。链表长度会越来越长,查找效率降低。
- 反之,加载因子越小,填满的元素越少。好处是冲突的机会减小了,但空间浪费多了。表中的数据将过于稀疏(很多空间还没用,就开始扩容了)
因此,必须在 “冲突的机会”与”空间利用率”之间寻找一种平衡与折中。
五、哈希表如何解决Hash冲突?
预防措施:1、计算hash值用扰动处理,使元素分布更均匀。
2、扩容,元素个数>扩容阈值(table容量*加载因子)。
数据结构:JDK1.7数组+ 单链表,JDK1.8数组+链表+红黑树。
六、为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键
1、包装类是final类型,具有不可变性,即保证了hash值的不可更改性。
2、内部重写了equals(),hashcode()方法,保证了hash值的准确性。
七、HashMap 键-值(key-value)都允许为空吗
都允许为空,键只允许一个为null,默认hash值为0;值可以为null,没有个数限制。
以上参考博客:(1)美团面试题:Hashmap的结构,1.7和1.8有哪些区别,史上最深入的分析_依本多情的博客-优快云博客
八、为什么Hashtable, ConcurrentHashMap 的 key和value 不能为null(并发角度分析)
ConcurrentHashmap和Hashtable都是支持并发的,二者规定key,value均不能为null,null的话,会抛出空指针异常。
为什么要这么设计?
当通过get(k)获取对应的value时,如果获取到的是null时,无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射。假如线程1调用m.contains(key)返回true,然后在调用m.get(key),这时的m可能已经不同了。因为线程2可能在线程1调用m.contains(key)时,删除了key节点,这样就会导致线程1得到的结果不明确,产生多线程安全问题,因此,Hashmap和ConcurrentHashMap的key和value不能为null。
HashMap允许key和value为null,在单线程时,调用contains()和get()不会出现问题,但是多线程下,就是线程不安全的。如果要保证线程安全,应该使用ConcurrentHashMap 。
九、HashMap和HashTable的区别
1、HashMap是线程不安全的,HashTable是线程安全的。
2、HashMap的key和value允许null值,但是key只允许有一个null值,并且hash值默认为0,HashTable的key和value不允许null值。
3、HashMap重新计算hash值,HashTable直接使用对象的hashCode。
4、HashMap计算位置是与运算,HashTable计算位置是取模运算。
5、HashMap默认容量是16,HashTable默认容量是11。
6、HashMap扩容时容量变为原来的2倍,HashTable扩容时容量变为原来的2倍+1。
十、使用hashmap需要注意哪些地方
十一、TreeMap
TreeMap是基于红黑树实现的,TreeMap默认是按key排序的,元素在插入TreeSet时会调用compareTo()方法用来排序,所以TreeSet中的元素对象要实现Comparable接口。
十二、LInkedHashMap
LInkedHashMap是基于HashMap实现的,默认是按插入顺序排序的(accessOrder=false),可以设置为按访问顺序排序(accessOrder=true)。
十三、concunrentHashMap
说明:put函数底层调用了putVal进行数据的插入,对于putVal函数的流程大体如下。
① 判断存储的key、value是否为空,若为空,则抛出异常,否则,进入步骤②
② 计算key的hash值,随后进入无限循环,该无限循环可以确保成功插入数据,若table表为空或者长度为0,则初始化table表,否则,进入步骤③
③ 根据key的hash值取出table表中的结点元素,若取出的结点为空(该桶为空),则使用CAS将key、value、hash值生成的结点放入桶中。否则,进入步骤④
④ 若该结点的的hash值为MOVED,则对该桶中的结点进行转移,否则,进入步骤⑤
⑤ 对桶中的第一个结点(即table表中的结点)进行加锁,对该桶进行遍历,桶中的结点的hash值与key值与给定的hash值和key值相等,则根据标识选择是否进行更新操作(用给定的value值
替换该结点的value值),若遍历完桶仍没有找到hash值与key值和指定的hash值与key值相等的结点,则直接新生一个结点并赋值为之前最后一个结点的下一个结点。进入步骤⑥
⑥ 若binCount值达到红黑树转化的阈值,则将桶中的结构转化为红黑树存储,最后,增加binCount的值。
https://www.cnblogs.com/banjinbaijiu/p/9147434.html
十四、set是如何实现不重复的
1、HashSet(无序、线程不安全) 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null
在HashSet中,基本的操作都是有HashMap底层实现的,因为HashSet底层是用HashMap存储数据的,Hashset的元素作为Hashmap的键,new Object()作为Hashmap的值。
为什么用new Object()作为Hashmap的值,为什么不用null来做值?
因为Hashmap删除的方法如果删除成功返回被删除的值,如果被删除的值不存在,会返回null,如果用null来做值,就区分不开删除成功还是被删除的值不存在。
2、TreeSet(有序,线程不安全) 是二差树实现的,Treeset中的数据是自动排好序的(根据compareTo()方法排序),不允许放入null值。
TreeSet的底层是TreeMap的keySet()。TreeSet的元素作为TreeMap的键,new Object()作为TreeMap的值
3、LInkedHashSet(有序,线程不安全)基于LInkedHashMap实现的。LInkedHashSet的元素作为LInkedHashMap的键,new Object()作为LInkedHashMap的值。
十五、ArrayLIst
初始容量10,扩容为原来的1.5倍。
ArrayList源码分析(基于JDK8)_augfun的博客-优快云博客_arraylist源码分析技巧
ArrayList如何实现线程安全:
1、Collections.synchronizedList
List<String> data=Collections.synchronizedList(new ArrayList<String>());
2、CopyOnWriteArrayList写时复制容器
十六、linkedList
双链表