Java集合基础知识点复习

发布于:2024-04-08 ⋅ 阅读:(159) ⋅ 点赞:(0)

Java提供的常见集合

主要分为两类:

  • 第一个是Collection 属于单列集合,第二个是Map 属于双列集合在Collection中有两个子接口List和Set。在我们平常开发的过程中用的比较多像list接口中的实现类ArrarList和LinkedList。 在Set接口中有实现类HashSet和TreeSet。
  • 在map接口中有很多的实现类,平时比较常见的是HashMap、TreeMap,还有一个线程安全的map:ConcurrentHashMap

List

ArrayList底层实现与扩容机制

ArrayList底层是用动态的数组实现的,初始容量为0,当第一次添加数据的时候才会初始化容量为10,在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组。

在这里插入图片描述


ArrayList list=new ArrayList(10)中的list扩容几次

在ArrayList的源码中提供了一个带参数的构造方法,这个参数就是指定的集合初始长度,所以给了一个10的参数,就是指定了集合的初始长度是10,这里面并没有扩容。

第二个构造方法是无参构造函数,默认创建一个空集合


如何实现数组和List之间的转换

数组转list,可以使用jdk自动的一个工具类Arrars,里面有一个asList方法可以转换为数组
List 转数组,可以直接调用list中的toArray方法,需要给一个参数,指定数组的类型,需要指定数组的长度。


用Arrays.asList转List后,如果修改了数组内容,list受影响吗?List用toArray转数组后,如果修改了List内容,数组受影响吗

Arrays.asList转换list之后,如果修改了数组的内容,list会受影响!,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址
list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响


ArrayList 和 LinkedList 的区别

主要是底层使用的数据结构不一样,ArrayList 是动态数组,LinkedList 是双向链表,这也导致了它们很多不同的特点。

从操作数据效率来说

ArrayList按照下标查询的时间复杂度O(1)【内存是连续的,根据寻址公式】, LinkedList不支持下标查询查找(未知索引): ArrayList需要遍历,链表也需要链表,时间复杂度都是O(n)
ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)
LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)

从内存空间占用来说

ArrayList底层是数组,内存连续,节省内存
LinkedList 是双向链表需要存储数据,和两个指针,更占用内存

从线程安全来说,ArrayList和LinkedList都不是线程安全的



如何解决ArrayList 和 LinkedList 不是线程安全问题

主要有两种解决方案:

  • 我们使用这个集合,优先在方法内使用,定义为局部变量,这样的话,就不会出现线程安全问题。

  • 如果非要在成员变量中使用的话,可以使用线程安全的集合来替代,但是使用同步锁属于下下策,会影响服务器性能,因为它会使并行变成串行。

    • ArrayList可以通过Collections 的 synchronizedList 方法将 ArrayList 转换成线程安全的容器后再使用。
    • LinkedList 换成ConcurrentLinkedQueue来使用

还可以使用ThreadLocal,把该属性绑定在本地线程中。



HashMap

HashMap的实现原理

JDK1.8之前采用的拉链法,数组+链表
JDK1.8之后采用数组+链表+红黑树,链表长度大于8且数组长度大于64则会从链表转化为红黑树


HashMap的扩容机制

  • 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到了扩容阈值(数组长度 * 0.75
  • 每次扩容的时候,都是扩容之前容量的2倍;扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
  • 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
  • 如果是红黑树,走红黑树的添加;如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,如果是0,该元素的位置则停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

hashMap的寻址算法

这个哈希方法首先计算出key的hashCode值,然后通过这个hash值右移16位后的二进制进行按位异或运算得到最后的hash值。
在putValue的方法中,计算数组下标的时候使用hash值与数组长度取模得到存储数据下标的位置,hashmap为了性能更好,并没有直接采用取模的方式,而是使用了数组长度-1 得到一个值,用这个值按位与运算hash值,最终得到数组的位置:e.hash & (newCap - 1)


为何HashMap的数组长度一定是2的次幂?

第一:
计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
第二:
扩容时重新计算索引效率更高:在进行扩容时会进行判断 hash值按位与运算
旧数组长租是否 == 0 如果等于0,则把元素留在原来位置 ,否则新位置是等于旧位置的下标+旧数组长度


hashmap在1.7的多线程死循环问题

jdk7的的数据结构是:数组+链表 在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环

比如说,现在有两个线程
线程一:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入

线程二也读取hashmap,直接进行扩容。因为是头插法,链表的顺序会颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程二执行结束。

当线程一再继续执行的时候就会出现死循环的问题。
线程一先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,所以B->A->B,形成循环。

当然,JDK 8 将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中死循环的问题。


HashSet与HashMap的区别

HashSet底层其实是用HashMap实现存储的, HashSet封装了一系列HashMap的方法. 依靠HashMap来存储元素值,(利用hashMap的key键进行存储), 而value值默认是一个Object类型的常量 所以HashSet也不允许出现重复值, 判断标准和HashMap判断标准相同, 两个元素的hashCode相等并且通过equals()方法返回true.


HashTable与HashMap的区别

第一,数据结构不一样,hashtable是数组+链表,hashmap在1.8之后改为了数组+链表+红黑树
第二,hashtable存储数据的时候都不能为null,而hashmap是可以的
第三,hash算法不同,hashtable是用本地修饰的hashcode值,这可能导致更多的哈希冲突,从而影响性能。而hashmap经常了二次hash(可以看上面的寻址算法)
第四,扩容方式不同,hashtable是当前容量翻倍+1,hashmap是当前容量翻

第五,hashtable是线程安全的,操作数据的时候加了锁synchronized,hashmap不是线程安全的,效率更高一些


hashmap线程安全问题

hashmap不是线程安全的,推荐采用ConcurrentHashMap进行使用,它是一个线程安全的HashMap
参考这篇博客


网站公告

今日签到

点亮在社区的每一天
去签到