集合集合总结

一、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

hashmap详解https://itimetraveler.github.io/2017/11/25/%E3%80%90Java%E3%80%91HashMap%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%88JDK1.8%EF%BC%89/#%E6%A0%91%E5%BD%A2%E5%8C%96%E6%96%B9%E6%B3%95treeifyBin

二、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中扩容后的元素新位置为原位置+原数组长度)_诺浅的博客-CSDN博客_为什么新的位置是原位置+原数组长度

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有哪些区别,史上最深入的分析_依本多情的博客-CSDN博客

八、为什么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的博客-CSDN博客_arraylist源码分析技巧

ArrayList如何实现线程安全:

1、Collections.synchronizedList

List<String> data=Collections.synchronizedList(new ArrayList<String>());

2、CopyOnWriteArrayList写时复制容器

十六、linkedList

双链表

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值