1.Java集合框架体系图
集合框架 | Collection单列集合 | List 有序,可重复 | Vector 数组结构,线程安全 | |
ArrayList 数组结构,非线程安全 | ||||
LinkedList 链表结构,非线程安全 | ||||
Set 无序,唯一 | HashSet 哈希表结构 | LinkedHashSet 哈希表+链表结构 | ||
TreeSet 红黑树结构 | ||||
Map双列集合 | HashTable 哈希表结构,线程安全 | Properties | ||
HashMap 哈希表结构,非线程安全 | LinkedHashMap 哈希表+链表结构 | LinkedHashMap的底层实现原理是通过继承HashMap并使用一个双向链表来维护元素的顺序。每个Entry节点都包含了前驱节点和后继节点的引用,这样可以在插入、删除和访问元素时,通过修改链表节点的引用来维护元素的顺序。 | ||
当元素被插入LinkedHashMap时,会在链表的尾部添加一个新的节点。当元素被访问时,LinkedHashMap会将该节点移动到链表的尾部,以保持访问顺序。这样,最近访问的元素会位于链表的尾部,而最早访问的元素会位于链表的头部。 通过哈希表和链表的结合,LinkedHashMap实现了快速的查找和保持元素顺序的特性。在哈希表中,通过键的哈希值进行快速查找,而在链表中,通过修改节点的引用来维护元素的顺序。 需要注意的是,LinkedHashMap提供了两种顺序模式:插入顺序和访问顺序。默认情况下,LinkedHashMap使用插入顺序,即元素按照插入的顺序排列。可以通过构造函数或者调用setAccessOrder(true)方法来设置为访问顺序,即元素按照访问的顺序排列。 | ||||
LinkedHashMap的底层实现使得它在保持元素顺序的同时,仍然具有快速的查找和插入性能。然而,由于需要维护额外的链表结构,LinkedHashMap相对于普通的HashMap会占用更多的内存空间。此外,在插入和删除元素时,相对于普通的HashMap,LinkedHashMap的性能略低。因此,在选择使用LinkedHashMap时,需要根据具体的需求权衡其优缺点。 | ||||
ConcurrentHashMap 哈希表结构,线程安全 | ||||
TreeMap 红黑树结构 |
2.ArrayList底层的实现原理是什么?
底层数据结构 | ArrayList底层是用动态的数组实现的 | |
数组(Array) | 是一种用连续的内存空间存储相同数据类型数据的线性数据结构 | |
数组下标为什么从0开始? | 寻址公式是:baseAddress+ i * dataTypeSize,计算下标的内存地址效率较高 | |
初始容量 | ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10 | |
扩容逻辑 | ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组 | |
添加逻辑 | 确保数组已使用长度(size)加1之后足够存下下一个数据 | |
计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍) | ||
确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。 | ||
返回添加成功布尔值 |
3.用Arrays.asList转List后,如果修改了数组内容,list受影响吗?
Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址
4.List用toArray转数组后,如果修改了List内容,数组受影响吗?
list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响
5.ArrayList 和 LinkedList 的区别是什么?
数据结构 | ArrayList 是动态数组的数据结构实现 | ||
![]() | |||
LinkedList 是双向链表的数据结构实现 | |||
![]() | |||
操作数据效率 | ArrayList按照下标查询的时间复杂度O(1)【内存是连续的,根据寻址公式】, LinkedList不支持下标查询 | ||
查找(未知索引) | ArrayList需要遍历,链表也需要遍历链表,时间复杂度都是O(n) | ||
新增和删除 | ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n) | ||
LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n) | |||
内存空间占用 | ArrayList | 底层是数组,内存连续,节省内存 | |
LinkedList | 是双向链表需要存储数据,和两个指针,更占用内存 | ||
线程安全 | 都不是线程安全的;如果需要保证线程安全,有两种方案: | 在方法内使用,局部变量则是线程安全的 | |
使用线程安全的Collections.synchronizedList()包装一下 |
6.HashMap实现原理
HashMap实现原理 | ||
数据结构 | 底层使用hash表数据结构,即数组和链表或红黑树 | |
当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标 | ||
存储时,如果出现hash值相同的key | 如果key相同,则覆盖原始值 | |
如果key不同(出现冲突),则将当前的key-value放入链表或红黑树中 | ||
获取时 | 直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。 | |
数据结构图 | ![]() | |
注: | 链表的长度大于8 且 数组长度大于64转换为红黑树 | |
红黑树 | 性质1 | 节点要么是红色,要么是黑色 |
性质2 | 根节点是黑色 | |
性质3 | 叶子节点都是黑色的空节点 | |
性质4 | 红黑树中红色节点的子节点都是黑色 | |
性质5 | 从任一节点到叶子节点的所有路径都包含相同数目的黑色节点 | |
保证平衡 | 在添加或删除节点的时候,如果不符合这些性质会发生旋转,以达到所有的性质 | |
例图 | ![]() |
7.HashMap的put方法的具体流程
HashMap的put方法的具体流程 | ||
常见属性 | DEFAULT_INITIAL_CAPACITY | 默认的初始容量 16 |
DEFAULT_LOAD_FACTOR | (在无参的构造函数中,设置了默认的加载因子是0.75)默认的加载因子 0.75 | |
扩容阈值 == 数组容量 * 加载因子 16*0.75=12 | ||
HashMap是懒惰加载,在创建对象时并没有初始化数组 | ||
添加数据流程图 | ![]() | |
put方法的具体流程 | 1. 判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化) | |
2. 根据键值key计算hash值得到数组索引 | ||
3. 判断table[i]==null | 直接新建节点添加 | |
4. 如果table[i]==null ,不成立 | 4.1 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value | |
4.2 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对 | ||
4.3 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操 作,遍历过程中若发现key已经存在直接覆盖value | ||
5. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容。 | ||
HashMap的扩容机制 | 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75) | |
每次扩容的时候,都是扩容之前容量的2倍; | ||
扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中 | ||
没有hash冲突的节点 | 则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置 | |
如果是红黑树 | 走红黑树的添加 | |
如果是链表 | 则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到[旧位置下标+ oldCap老的数组容量大小] |
8.HashMap的寻址算法
扰动算法,是hash值更加均匀,减少hash冲突
(n-1)&hash : 得到数组中的索引,代替取模,性能更好
数组长度必须是2的n次幂(计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
;扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则(可能拆分链表)新位置 = 旧位置下标+ oldCap老的数组容量大小
)
9.HashMap在1.7情况下的多线程死循环问题
在jdk1.7的HashMap中在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环
比如说,现在有两个线程
线程一:读取到当前的HashMap数据,数据中一个链表,在准备扩容时,线程二介入
线程二:也读取HashMap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程二执行结束。
线程一:继续执行的时候就会出现死循环的问题。
线程一先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,所以B->A->B,形成循环。
当然,JDK 8 将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中死循环的问题。