(一)JU集合框架层级图
1、Collection层级图
2、Map层级图
(二)Collection实现类关键点
1、ArrayList与LinkedList的实现和区别?
实现:
(1)ArrayList底层是Object数组,默认初始容量10,扩容机制为当前容量的1.5倍,初始容量可使用构造函数
指定。
(2)LinkedList底层是Node节点,利用内部成员变量pre和next进行双向链接,最外层first和last节点实现双
向循环链表。
区别:
(1)底层存储结构不一样:ArrayList为Object数组,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的存储性能和特性?
(1)ArrayList底层存储为Object类型数组,查询较快,但插入和删除时非常麻烦
(2)LinkedList底层Node类型的双向链表(first和last)结构,插入和删除较快,只需要将Node的前
后prev和next链接,但查询较慢;
(3)ArrayList和Vector都是Object类型数组,可设置存储容量,也可自动扩容,且扩容机制不同,
可用序号对数组元素索引,但插入和删除需要移动其它元素,性能较差;
(4)Vector使用了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冲突如何解决和链表为什么转红黑树?
数据结构:
(1)HashMap底层存储结构为Node数组+链表+红黑树;
(2)Node为内部类,其成员变量为:key、value、next、hash;
解决hash冲突:
(1)HashMap在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的幂?
(1)HashMap的数组长度在构造函数指定初始容量时会检测是否为2的幂即tableSizeFor(n);
(2)HashMap的hash算法即扰动算法,利用了右移16位和异或操作来实现hash,编译jvm快速计算;
(3)扩容时的迁移操作也是利用二进制来实现快速定位;
6、HashMap和HashTable的区别?
相同点:存储的都是key-value键值对;
不同点:
(1)HashMap允许key-value均为null,HashTable不允许任何一个为null,否则报空指针;
(2)HashMap不是线程安全,不同步,HashTable是线程安全的,底层涉及到数据的方法均加了synchronized关键字;
(3)HashMap继承于AbstractMap类,HashTable继承于Dictionary类;
(4)HashMap迭代器为Iterator(fail-fast),HashTable迭代器为Enumerator(不是fail-fast),
多线程遍历操作(put、remove)HashMap会报并发修改异常ConcurrentModificationException。
(5)HashMap默认容量值16,扩容是翻倍扩容,HashTable默认容量大小是11(构造方法中写死),扩容是翻倍+1;
(6)HashMap的hash算法为对key的hashcode进行右移16位,然后异或;hashTable的hash算法直接使用key的hashcode作为hash值。
7、LinkedHashMap的基本原理和哪两种有序?
基本原理:
(1)LinkedHashMap继承于HashMap,存储原理大致与其父类HashMap相同,但内部Entry多了before和after成员变量,用于链接Entry节点即链表顺序;
(2)LinkedHashMap有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,每次操作后都需要调用判断最久数据方法,然后再根据容量去判断是否调用删除最久数据方法。