ArrayList
概述
ArrayList是 Java 集合框架中使用最为普遍的集合类之一。ArrayList 是一种 List 实现,允许包括null在内的所有元素,它的内部用一个动态数组来存储元素,因此 ArrayList 能够在添加和移除元素的时候进行动态的扩展和缩减。
ArrayList有容量限制。超出限制时会增加50%容量,用System.arraycopy()复制到新的数组,因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。
ArrayList按数组下标访问元素–get(i)/set(i,e) 的性能很高,这是数组的基本优势。
ArrayList直接在数组末尾加入元素–add(e)的性能也高,但如果按下标插入、删除元素–add(i,e), remove(i), remove(e),则要用System.arraycopy()来移动部分受影响的元素,性能就变差了,这是基本劣势。
定义
|
|
ArrayList实现
对应ArrayList而言,它实现List接口、底层使用数组保存所有元素,其操作基本上都是对数组的操作。下面我们来分析ArrayList的源码:
底层数组实现
|
|
构造方法
ArrayList提供了三种方式的构造器:
- 构造一个默认初始容量为10的空列表;
- 构造一个指定初始容量的空列表;
- 构造一个包含指定collection集合的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。
|
|
存储
ArrayList提供了set、add、addAll这些方法添加元素
set()
|
|
add()
|
|
addAll()
|
|
读取元素
|
|
删除元素
|
|
注意:从数组中移除元素的操作,也会导致被移除的元素以后的所有元素的向左移动一个位置。
调整数组容量
每次向ArrayList中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity()来实现。在实际添加大量元素前,我们也可以使用该方法手动增加ArrayList的容量,以减少递增式再分配的数量。
|
|
从上述代码可以看出,数组进行扩容时,会将老数组中的元素拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素有多少时,要在构造ArrayList实例时就指定其容量大小,以避免数组扩容的发送。或者根据实际需求,通过调用ensureCapacity()方法手动增加ArrayList实例的容量。
ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能,可以通过trimToSize方法来实现:
|
|
Fail-Fast机制
ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会抛出失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
|
|
ArrayList遍历
有三种方法可以遍历ArrayList数组,分别是for、foreach、Iterator。
|
|
LinkedList
LinkedList是一种基于链表结构的一中List,具体是基于双向循环列表设计的。
定义
|
|
LinkedList实现
底层存储
|
|
Node表示链表的节点对象。
|
|
Node是LinkedList的内部类,其中定义了当前存储的元素,以及该元素的上一个元素和下一个元素。
构造方法
|
|
add()
|
|
addAll()
|
|
删除
|
|
修改
|
|
查询
|
|
基于双向循环链表实现的LinkedList,通过索引Index的操作时低效的,index所对应的元素越靠近中间所费时间越长。而向链表两端插入和删除元素则是非常高效的(如果不是两端的话,都需要对链表进行遍历查找)。
ArrayList vs LinkedList
- ArrayList底层实现是数组,LinkedList实现是链表
- ArrayList的查找效率高于LinkedList
- LinkedList的增删效率高于ArrayList
ArrayList操作:
- 查询操作的时间复杂度是O(1)
- 增删操作的时间复杂度是O(n)
LinkedList:
- 查询操作的时间复杂度是O(n)
- 增删操作的时间复杂度是O(1)
面试题
ArrayList的大小是如何自动增加的?
当我们试图在ArrayList中增加一个对象时,首先会检查ArrayList的容量,已确保已存在的数组中有足够的容量来存储新的对象。如果没有足够容量的话,就会新建一个长度更长的数组(长度是原数组长度的1.5倍),然后使用Arrays.copyOf()方法将旧的数组赋值到新的数组中去,并将现有的数组引用指向新的数组。
|
|
什么情况下使用ArrayList?什么情况下使用LinkedList?
多数情况下,当你遇到访问元素比插入或删除元素操作更频繁的时候,你应该使用ArrayList。当你遇到插入或者删除元素操作更加频繁,或者根本不需要访问元素的时候,你应该使用LinkedList。主要原因在于,在ArrayList中访问元素的最糟糕的时间复杂度为1,而在LinkedList中可能就是n;在LinkedList中插入和删除的时间复杂度为1,而在ArrayList中可能就是n。
当传递ArrayList到某个方法中,或者某个方法返回ArrayList,什么时候要考虑安全隐患?如何修护安全违规这个问题?
当ArrayList被当做参数传递到某个方法中,如果ArrayList在没有被复制的情况下直接被分配给成员变量,那么久可能发生这种情况,即当原始的ArrayList被改变时,传递到这个方法的数组也会改变。下面来看看实例:
安全隐患的情况:
|
|
从结果可以看出,原数组和成员变量数组同时发送改变,这是因为在setList()方法中是将数组的引用赋值给了成员变量。
修复安全隐患后的代码为:
|
|
从结果可以看出,现在原数组和成员变量mList相互独立,改变自己的同时不会改变对方的数组内容。
数组[]也是如此
如何复制一个ArrayList到另一个ArrayList中去?
- 使用clone()方法,
- 使用ArrayList构造方法,
- 使用Collection的copy方法。
|
|
在索引中ArrayList的增加或者删除某个对象的运行过程?效率很低吗?解释一下为什么?
在ArrayList中增加或删除元素的时候要调用System.arrayCopy()这个数值拷贝函数,每次增加或删除元素都要进行数组的拷贝操作,相对效率较低。如果遇到频繁插入或删除操作的时候,可以考虑使用LinkedList来代替。
在ArrayList的某个索引i处添加元素:
|
|
在ArrayList的某个索引i处删除元素:
|
|