集合框架核心知识点——非线程安全JU

(一)JU集合框架层级图

1、Collection层级图

Collection常见实现类

2、Map层级图

Map各实现类层级图

(二)Collection实现类关键点

1、ArrayList与LinkedList的实现和区别?

实现:

1ArrayList底层是Object数组,默认初始容量10,扩容机制为当前容量的1.5,初始容量可使用构造函数
指定。
(2LinkedList底层是Node节点,利用内部成员变量pre和next进行双向链接,最外层first和last节点实现双
向循环链表。

区别:

1)底层存储结构不一样:ArrayListObject数组,LinkedList底层为Node内部类节点存储;2)有无扩容机制:ArrayList是原容量+原容量>>1,LinkedList无扩容机制,链表结构,但均不能超过
Integer的最大值;3)使用场景不一样:ArrayList因底层是数组,适用于数据较稳定场景如配置数据、少量缓存,LinkedList因
链表结构,适用于数据可变化的场景;4)都是单线程操作,多线程环境会导致并发修改异常java.util.ConcurrentModificationException

2、ArrayList无参构造初始化和第一次add时发生了些什么变化?

无参构造初始化:

1)无参构造进行初始化,主要是对Object[] elementData变量进行初始化为数组类型的空对象;2)重要成员变量情况:size值为0,数组长度为0,可用容量为0,,存储数组目前为空的数组对象;

第一次add时变化:

1)触发扩容操作,因无参构造进行初始化,存储数组仅进行空的数组对象赋值,此时会触发默认容量赋值10
(也是扩容逻辑判定并赋值);2)重要成员变量情况:size此时为1,数组长度为10,可用容量为9;

3、ArrayList数组增加到11个时,数组长度(大小)是多少?

场景:默认使用无参构造初始化

使用无参构造,则默认容量为10,add方法是每次存储前,会对存储数组进行对比,是否超过当前数组最大值。
如果超过容量值,则先进行扩容然后再存储值,此时数组长度为15,可用容量为4;反之则直接存储,此时数组长度为10,可用容量为当前容量-1;
结论:在此场景下,显然在增加到11个时,数组容量不足以存储第11个值,会先进行扩容到15,然后存储值,此时数组长度为15,可用容量为4;

4、ArrayList和Vector的区别?

区别:
四个方面,如下:

1)构造方法初始化结果不同;
(2)扩容机制不一样;3)底层存储都是数组,不便于插入和删除,查询都较快;4)线程安全与不安全;

ArrayList

1)实现原理:采用对象数组(Object类型)实现,默认构造方法创建了一个空数组;2)第一次添加元素,扩展容量为10,之后的扩充算法:原来数组大小+原来数组的一半;3)当插入、删除位置比较靠前时,与链表比较,不适合进行删除或插入操作;4)多线程中使用不安全,适合在单线程访问时使用,效率较高

Vector

1)实现原理:采用数组对象(Object类型)实现,默认构造方法创建了一个大小为10的对象数组;2)扩容机制:当增量为0,扩充为原来的2,当增量大于0,扩充为原来大小+增量;3)当插入、删除位置比较靠前时,与链表比较,不适合删除或插入操作;4)线程安全(synchronized关键字),适合在多线程访问时使用,效率较低。

5、ArrayList、Vector和LinkedList的存储性能和特性?

1ArrayList底层存储为Object类型数组,查询较快,但插入和删除时非常麻烦
(2LinkedList底层Node类型的双向链表(first和last)结构,插入和删除较快,只需要将Node的前
后prev和next链接,但查询较慢;3ArrayListVector都是Object类型数组,可设置存储容量,也可自动扩容,且扩容机制不同,
可用序号对数组元素索引,但插入和删除需要移动其它元素,性能较差;4Vector使用了synchronized方法(线程安全),性能相对ArrayList差一点。

6、快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?

(1)快速失败(fail-fast)
      在用迭代器遍历一个集合对象时,遍历过程中进行增加、删除、修改等操作,会报并发修改异常。
原理:

  迭代器的遍历是直接访问集合中的内容且遍历过程中使用一个modCount变量,集合在被遍历期间如果内
 容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,会检测
 modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

场景:
      java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

PS:
      异常的抛出条件是检测到modCount!=expectedmodCount。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。即不能单纯的用此异常来判断并发有效性。

(2)安全失败(fail-safe)
      采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
原理:

  由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,
  所以不会触发ConcurrentModificationException

场景:
      java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

PS:基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即其它线程修改后的值在此迭代器中是无法得到更新的。

(三)Map实现类关键点

1、HashMap底层原理?

HashMap底层存储为Node数组+链表+红黑树,主要是put方法对键值对进行存储,步骤如下:

1)首先是根据key进行Hash即根据key的hashcode进行右移16,然后再与其进行异或(高低位);2)然后判断是否初始化过,table为空则调用resize方法,此时会进行初始化逻辑,并不会走扩容逻辑;3)再根据扰动算法(即hash)得到的int值对当前容量进行按位与(&)运算,(m-1)&hash值确定桶位置;4)根据桶位置进行空桶插入、链表插入(尾插法)、树节点插入和替换插入,此过程会判定是否进行扩容和树化(链表转树化)

HashMap的get方法,与put类似,根据key的扰动算法,然后再进行按位与运算,最终利用key的hash和equals方法去匹配node, 此过程会存在空桶比较、链表比较、红黑树比较,最终返回value,找不到即返回null。

2、HashMap数据结构、hash冲突如何解决和链表为什么转红黑树?

数据结构:

1HashMap底层存储结构为Node数组+链表+红黑树;2Node为内部类,其成员变量为:key、value、next、hash;

解决hash冲突:

1HashMap在put时,发现key经过扰动算法和按位与运算,出现桶位置一样,则根据当前桶数据结构是链表和红黑树进行尾部链接(追加);2)如果当前桶为链表,则进行key的hashcode匹配和equals匹配,一致则进行新值替换,否则进行尾插法链接,不排除转红黑树;3)如果当前桶为红黑树,也会进行key的hashcode和equals匹配,一致则进行替换,否则进行追加,可能伴随着树调整即左旋和右旋;4)链表与桶之间的转化阈值:数组长度大于64和链表长度大于8,则数据结构转为红黑树;解树为长度小于6;

链表转红黑树:

1)链表长度大于8和数组长度大于64,则将链表转为红黑树结构;2)当链表长度大于8,单个Node值遍历,时间复杂度为O(N),红黑树时间复杂度O(logN),此时红黑树结构查询效率会更高;3)数组长度大于64,则是为了充分将数据利用,8是泊松分布统计经验值;

3、HashMap什么时候触发扩容,resize扩容方法如何实现和扩容时避免rehash的优化?

触发扩容resize方法的条件:

1)第一次put时,如果table未初始化时会触发,此时只会执行初始化逻辑,并不会走扩容逻辑;2)当put键值对时,当前数量>扩容阈值时会触发扩容操作;3)当链表大于等于8且数组长度小于64,此时不对链表进行树化,而是对数组进行扩容,均满足时才会转化为红黑树。

resize实现方式:

1)先判断数组table是否已被初始化,如果为空,则进行数组初始化和阈值计算等;2)如果table不为空,则进行下一步扩容,基于当前容量进行翻倍扩容,对链表和红黑树都进行迁移(可能会存在红黑树解化);

扩容避免rehash优化:

1)利用高低位进行归类,然后统一迁移。
(2)高位为1,扩容后的桶位置为:原数组大小+原索引,低位为0,扩容后的位置为:新数组中的原索引;

PS:避免触发扩容优化:初始容量=(实际存储数据/负载因子) +1;

4、jdk1.8之前并发操作HashMap时为什么会有死循环的问题?

1)jdk1.7在扩容时,数据迁移时采用单个rehash,在链表迁移时,使用头插法转移到新的数组中;2)多线程并发操作,可能HashMap正在迁移过程中,另一线程也在put时发生扩容,此时可能会造成链表循环结果;3)jdk1.8在扩容时,对链表进行高低位归类且使用尾插法,解决了链表循环问题;

5、HashMap的数组长度为什么要保证是2的幂?

1HashMap的数组长度在构造函数指定初始容量时会检测是否为2的幂即tableSizeFor(n);2HashMap的hash算法即扰动算法,利用了右移16位和异或操作来实现hash,编译jvm快速计算;3)扩容时的迁移操作也是利用二进制来实现快速定位;

6、HashMap和HashTable的区别?

相同点:存储的都是key-value键值对;
不同点:

1HashMap允许key-value均为null,HashTable不允许任何一个为null,否则报空指针;2HashMap不是线程安全,不同步,HashTable是线程安全的,底层涉及到数据的方法均加了synchronized关键字;3HashMap继承于AbstractMap,HashTable继承于Dictionary;4HashMap迭代器为Iterator(fail-fast),HashTable迭代器为Enumerator(不是fail-fast),
    多线程遍历操作(put、remove)HashMap会报并发修改异常ConcurrentModificationException。
(5HashMap默认容量值16,扩容是翻倍扩容,HashTable默认容量大小是11(构造方法中写死),扩容是翻倍+1;6HashMap的hash算法为对key的hashcode进行右移16,然后异或;hashTable的hash算法直接使用key的hashcode作为hash值。

7、LinkedHashMap的基本原理和哪两种有序?

基本原理:

1LinkedHashMap继承于HashMap,存储原理大致与其父类HashMap相同,但内部Entry多了before和after成员变量,用于链接Entry节点即链表顺序;2LinkedHashMap有head和tail外部成员变量,用于实现双向循环列表链表;
两种排序:
(1)插入排序即按照put的顺序,默认为插入排序(false),可通过构造函数指定排序方式;2)访问排序即按照最新操作顺序排序如get后,K-V键值对在最前面。

PS:LinkedHashMap继承HashMap,其操作方法基本沿用父类HashMap,不同之处在于顺序控制,在方法中重写了父类的部分方法。

8、如何用LinkedHashMap实现LRU?

1)自定义一个类,继承于LinkedHashMap,使用其put、get、remove方法;2)重写linkedHashMap中的removeEldestEntry方法即用于LinkedHashMap中重写使用,将第一个
Entry头节点传入进去(LinkedHashMap本身有head);

PS:自定义实现LRU

1)按照LinkedHashMap的思路去实现,沿用HashMap的存储结构,定义头、尾指针、容量变量、entry类等;2)定义判断最久数据的方法如removeEldestEntry、定义get\remove\put方法\删除最久数据方法,全手写实现存储,最底层使用HashMap存储数据;3)自定义实现get\remove\put,每次操作后都需要调用判断最久数据方法,然后再根据容量去判断是否调用删除最久数据方法。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进击的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值