夺命追问带你深入了解ArrayList与LinkedList

发布时间:2024年01月11日

目录

一、ArrayList

问题1:说一下JDK1.7与1.8 ArrayList有什么区别?

问2:说一下ArrayList的扩容机制?

问3:下面这段代码会将数组扩容到多少?

问4:说说迭代器Iterator的两种规则:fail-fast和fail-safe

问5:简单说说fail-fast的源码

二、LinkedList

问1:ArrayList与LinkedList的比较?

问2:ArrayList与LinkedList 查询、增删性能比较?

问3:什么是局部性原理?


一、ArrayList

问题1:说一下JDK1.7与1.8 ArrayList有什么区别?

答:

(1)JDK7:JDK7的ArrayList类似于单例模式的饿汉式,new ArrayList();时就创建长度为10的数组,当你添加到第11个元素时进行数组的扩容,扩容成当前的1.5倍,也就是长度15的数组。

(2)JDK8:JDK8的ArrayList类似于单例模式的懒汉式,new ArrayList();仅仅是将elementData初始化为{},也就是一个长度为0的数组。当第一次add()的时候,底层才创建了长度为10的数组。添加到第11个元素时数组进行扩容,扩容成当前的1.5倍,也就是长度15的数组。

问2:说一下ArrayList的扩容机制?

答:一开始new ArrayList(); 底层创建容量为0的数组,当我们add()第一个元素时,触发第一次扩容,将容量0变为10,当数组容量存满10个,add()第11个元素时触发第二次扩容,扩容为原理的1.5倍,也就是15。当我们add()到第16个元素,触发第三次扩容,但注意,扩容1.5倍并不准确,拿第三次扩容举例,15 * 1.5 = 22.5,结果发现带小数了。其实底层运用的是移位,15 >> 1(15右移一位,相当于除2)等于7,结果7再加上原始的容量15,就是新的容量,结果是22。

问3:下面这段代码会将数组扩容到多少?

ArrayList<Integer> list = new ArrayList<>();
list.addAll(List.of(1,2,3,4,5,6,7,8,9,10,11));

答:正常我们分析一开始初始化容量是0,一口气放11个元素,那肯定扩容成15了。但结果却是11,为什么呢?因为addAll运行的规律是,当你原始的容量不够时,它会在下一次扩容容量的大小(10,第一次是0,下一次就是10)和我们addAll的元素个数二者之间取一个较大值作为下一次容量。那就是10和11比,那结果就是11。

这就是addAll方法特殊的运行规律。

问4:说说迭代器Iterator的两种规则:fail-fast和fail-safe

答:我们都知道,ArrayList使用迭代器或foreach遍历时,其它线程不可改变其元素,如果改变则会抛出异常,而List其实有两种规则来指定此问题。

fail-fast:一旦发现遍历的同时其它人来修改,则立刻抛异常。

fail-safe:发现遍历的同时其它人来修改,应当能有应对策略,例如牺牲一致性让整个遍历运行完成。例如遍历有5个元素的集合,分别是abcde,假如遍历到c的时候,我们添加一个f,但发现并不会报错,只是结果还是旧的abcde,但是当你整个for循环遍历完成,再打印一次list,则会发现有f元素了。

ArrayList是fail-fast的典型代表,遍历的同时不能修改,尽快失败抛出异常。

CopyOnWriteArrayList是fail-safe的典型代表,遍历的同时可以修改,原理是读写分离。底层就是弄两个数组,一个是用来读的,另一个新数组是用来写新元素的。

问5:简单说说fail-fast的源码

答:foreach遍历集合,其实是走的Iterator,首先判断hasNext(),如果没有了则终止循环,否则next()获取元素时,都要check一下集合元素个数是否变化了,如果变化了,则抛出异常。

modCount是集合添加元素、删除元素的次数,expectedModCount是预期的修改次数。增删操作会使得modCount+1,不等于expetedModCount,所以抛出异常。

没有使用list.iterator时调用的是ArrayList自己的remove方法,并不会同步这两个值,导致抛出异常。调用了ArrayList.iterator之后,此后再remove,remove方法中有让这两个值相等的操作。

迭代器的remove方法会修改expectedModCount,从而使modCount与之相等。

也就是说,如果你要在迭代中移除元素,那你最好用迭代器,别用foreach,因为foreach用list.remove();会报错,但迭代器中的iterator.remove();不会报错。

注意:

expectedModCount:预期修改次数,集合假如一开始add() 5个元素,那expectedModCount就是5。

modCount:在循环的时候如果增删,这个变量就会+1。

二、LinkedList

问1:ArrayList与LinkedList的比较?

答:

(1)ArrayList

① 底层是数组,需要连续内存。

② 随机访问快(指根据下标访问)。

③ 尾部插入、删除性能可以,其它部分插入、删除会移动数据,因此性能会低。

④ 可以利用cpu缓存,局部性原理。

(2)LinkedList

① 底层是双向链表,无需连续内存。

② 随机访问慢(要沿着链表遍历)。

③ 头尾部插入删除性能高。

④ 占用内存多。

解释链表为何查询慢:因为每个节点只知道上一个元素和下一个元素是什么,我如果想找到4,那我从1开始找,1下一个元素是2,2下一个元素是3,3下一个元素是4这么找,沿着链表遍历。

而删除为什么快,双向链表可以想象成小朋友手拉手站成一排,如果其中一个小朋友退出游戏,那么直接让旁边两个小朋友拉手即可,跟其他的小朋友没关系。删除第3个元素,只跟第2个和第4个元素有关系,只需要把第2个元素的地址值给第4个,再将第4个元素的地址值给第2个即可。

问2:ArrayList与LinkedList 查询、增删性能比较?

答:放弃ArrayList增删慢,查询快。LinkedList增删快,查询慢这种说法。

首先我们要明确一个概念,什么是随机访问?什么是查询?

随机访问:是随便根据索引去挑一个。

查询:根据元素的内容去元素里找。

按查询来说,ArrayList和LinkedList 根据元素内容去找的话,时间复杂度都差不多,都是O(n)。换句话说,这哥俩都不太适合用来做查询,要真想做查询,就用HashMap或者TreeMap这种高效的数据结构,它们的哈希表和红黑树都更适合用来做查询。

按随机访问来说,ArrayList肯定比LinkedList要快,因为带索引查询,而双向链表只能沿着链表一点点找。

增删的话,ArrayList尾部增删都很快,因为不用移动其它元素,直接增加或删除即可,如果其它位置增删,比如头部增删,那其它的元素都需要移动位置来配合你。而LinkedList只是头尾插入删除性能比较高,但链表的中间去增删元素,它的性能实在不敢恭维。测试代码结果是LinkedList比ArrayList耗费时间多出6倍。

为什么LinkedList中间插入元素会那么慢?

原因是你需要先定位到中间,那必然通过LinkedList的next一个个的找到中间位置元素,这个查找的过程,移动指针的过程是非常慢的。当然定位到了以后做增删操作是非常快的,无非是链表的指针重新改变一下。

问3:什么是局部性原理?

答:局部性原理就是我们查询一个元素时,可以将此数据相邻的几个元素都加入到缓存中,因为它会做出一种假设,如果你查询的是1,那么你有可能马上会用到2、3、4、5,那我就一起都放入缓存中。因为ArrayList底层是数组,所以它可以实现这种局部性原理的方式来优化。但LinkedList不行,因为双向链表的缘故,有可能你查询的1元素,它的指针指向下一个元素是2,但有可能1和2元素并不相邻,中间可能差的有点远。所以缓存中并没有把你接下来的元素2加载进缓存中。

文章来源:https://blog.csdn.net/qq_38196449/article/details/135518476
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。