JAVA集合(学习)

本文详细解析了Java集合框架中的各种数据结构,如List(如ArrayList和LinkedList)、Set(如HashSet和LinkedHashSet)、Map(如HashMap和LinkedHashMap)以及它们的底层实现原理,包括哈希表、链表和红黑树。还讨论了线程安全、排序顺序、性能优化和并发问题,如ConcurrentHashMap和死锁问题的解决方案。
摘要由CSDN通过智能技术生成

1.Java集合框架体系图

集合框架Collection单列集合List 有序,可重复Vector 数组结构,线程安全
ArrayList 数组结构,非线程安全
LinkedList 链表结构,非线程安全
Set 无序,唯一HashSet   哈希表结构LinkedHashSet   哈希表+链表结构
TreeSet    红黑树结构
Map双列集合HashTable 哈希表结构,线程安全Properties
HashMap 哈希表结构,非线程安全LinkedHashMap 哈希表+链表结构

LinkedHashMap的底层实现原理是通过继承HashMap并使用一个双向链表来维护元素的顺序。每个Entry节点都包含了前驱节点和后继节点的引用,这样可以在插入、删除和访问元素时,通过修改链表节点的引用来维护元素的顺序。
具体来说,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中死循环的问题。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值