“ArrayList 和 LinkedList 在新增、删除元素时,LinkedList 的效率要高于 ArrayList,而在遍历的时候,ArrayList 的效率要高于LinkedList。”这个回答是否准确呢?今天这一讲就带你验证。
ArrayList 实现了 List 接口,继承了 AbstractList 抽象类,底层是数组实现的,并且实现了自增扩容数组大小。ArrayList 还实现了 Cloneable 接口和 Serializable 接口,所以他可以实现克隆和序列化。ArrayList 还实现了 RandomAccess 接口,能实现快速随机访问。
ArrayList 属性主要由数组长度 size、对象数组elementData、初始化容量 default_capacity 等组成, 其中初始化容量默认大小为 10。
从 ArrayList 属性来看,elementData 被关键字 transient 修饰了。由于 ArrayList 的数组是基于动态扩增的,所以并不是所有被分配的内存空间都存储了数据。如果采用外部序列化法实现数组的序列化,会序列化整个数组(ArrayList 其实是实现了序列化接口)ArrayList 为了避免这些没有存储数据的内存空间被序列化,内部提供了两个私有方法 writeObject 以及 readObject 来自我完成序列化与反序列化,从而在序列化与反序列化数组时节省了空间和时间。因此使用 transient 修饰数组,是防止对象数组被其他外部方法序列化。
ArrayList 类实现了三个构造函数,第一个是创建 ArrayList 对象时,传入一个初始化值;第二个是默认创建一个空数组对象;第三个是传入一个集合类型进行初始化。
当 ArrayList 新增元素时,如果所存储的元素已经超过其已有大小,它会计算元素大小后再进行动态扩容,数组的扩容会导致整个数组进行一次内存复制。因此,我们在初始化 ArrayList 时,可以通过第一个构造函数合理指定数组初始大小,这样有助于减少数组的扩容次数,从而提高系统性能。
ArrayList 新增元素的方法有两种,一种是直接将元素加到数组的末尾,另外一种是添加元素到任意位置。
两个方法的相同之处是在添加元素之前,都会先确认容量大小,如果容量够大,就不用进行扩容;如果容量不够大,就会按照原来数组的 1.5 倍大小进行扩容,在扩容之后需要将数组复制到新分配的内存地址。
当然,两个方法也有不同之处,添加元素到任意位置,会导致在该位置后的所有元素都需要重新排列,而将元素添加到数组的末尾,在没有发生扩容的前提下,是不会有元素复制排序过程的。
如果我们在初始化时就比较清楚存储数据的大小,就可以在 ArrayList 初始化时指定数组容量大小,并且在添加元素时,只在数组末尾添加元素,那么 ArrayList 在大量新增元素的场景下,性能并不会变差,反而比其他 List 集合的性能要好。
ArrayList 的删除方法和添加任意位置元素的方法是有些相同的。ArrayList 在每一次有效的删除元素操作之后,都要进行数组的重组,并且删除的元素位置越靠前,数组重组的开销就越大。
由于 ArrayList 是基于数组实现的,所以在获取元素的时候是非常快捷的。
虽然 LinkedList 与 ArrayList 都是 List 类型的集合,但 LinkedList 的实现原理却和ArrayList 大相径庭,使用场景也不太一样。
LinkedList 是基于双向链表数据结构实现的,LinkedList 定义了一个 Node 结构,Node 结构中包含了 3 个部分:元素内容 item、前指针 prev以及后指针 next,代码如下。
LinkedList 就是由 Node 结构对象连接而成的一个双向链表。在 JDK1.7 之前,LinkedList 中只包含了一个 Entry 结构的 header 属性,并在初始化的时候默认创建一个空的Entry,用来做 header,前后指针指向自己,形成一个循环双向链表。
在 JDK1.7 之后,LinkedList 做了很大的改动,对链表进行了优化。链表的 Entry 结构换成了Node,内部组成基本没有改变,但 LinkedList 里面的 header 属性去掉了,新增了一个 Node结构的 first 属性和一个 Node 结构的 last 属性。这样做有以下几点好处:
first/last 属性能更清晰地表达链表的链头和链尾概念;
first/last 方式可以在初始化 LinkedList的时候节省 new 一个 Entry;
first/last 方式最重要的性能优化是链头和链尾的插入删除操作更加快捷了。
LinkedList 类实现了 List 接口、Deque 接口,同时继承了 AbstractSequentialLis抽象类,LinkedList 既实现了 List 类型又有Queue 类型的特点;LinkedList 也实现了 Cloneable 和Serializable 接口,同 ArrayList 一样,可以实现克隆和序列化。
由于 LinkedList 存储数据的内存地址是不连续的,而是通过指针来定位不连续地址,因此,LinkedList 不支持随机快速访问LinkedList 也就不能实现 RandomAccess接口。
我们前面讲到了 LinkedList 的两个重要属性 first/last 属性,其实还有一个 size 属性。我们可以看到这三个属性都被 transient 修饰了,原因很简单,我们在序列化的时候不会只对头尾进行序列化,所以 LinkedList 也是自行实现readObject 和 writeObject 进行序列化。
LinkedList 添加元素的实现很简洁,但添加的方式却有很多种。默认的 add (Ee) 方法是将添加的元素加到队尾,首先是将 last 元素置换到临时变量中,生成一个新的 Node 节点对象,然后将last 引用指向新节点对象,之前的 last 对象的前指针指向新节点对象。
LinkedList 也有添加元素到任意位置的方法,如果我们是将元素添加到任意两个元素的中间位置,添加元素操作只会改变前后元素的前后指针,指针将会指向添加的新元素,所以相比ArrayList 的添加操作来说,LinkedList 的性能优势明显。
在 LinkedList 删除元素的操作中,我们首先要通过循环找到要删除的元素,如果要删除的位置处于 List 的前半段,就从前往后找;若其位置处于后半段,就从后往前找。这样做的话,无论要删除较为靠前或较为靠后的元素都是非常高效的,但如果 List 拥有大量元素,移除的元素又在 List 的中间段,那效率相对来说会很低。
如果现在还有人跟你说,“ArrayList 和 LinkedList 在新增、删除元素时LinkedList 的效率要高于 ArrayList,而在遍历的时候,ArrayList 的效率要高于 LinkedList”,你还会表示赞同吗?
从集合头部位置新增元素花费时间
ArrayList>LinkedList
从集合中间位置新增元素花费时间
ArrayList 从集合尾部位置新增元素花费时间 ArrayList 通过这组测试,我们可以知道 LinkedList 添加元素的效率未必要高于 ArrayList。 由于 ArrayList 是数组实现的,而数组是一块连续的内存空间,在添加元素到数组头部的时候,需要对头部以后的数据进行复制重排,所以效率很低;而 LinkedList 是基于链表实现,在添加元素的时候,首先会通过循环查找到添加元素的位置,如果要添加的位置处于 List 的前半段,就从前往后找;若其位置处于后半段,就从后往前找。因此 LinkedList 添加元素到头部是非常高效的。 同上可知,ArrayList 在添加元素到数组中间时,同样有部分数据需要复制重排,效率也不是很高;LinkedList 将元素添加到中间位置,是添加元素最低效率的,因为靠近中间位置,在添加元素之前的循环查找是遍历元素最多的操作。而在添加元素到尾部的操作中,我们发现,在没有扩容的情况下,ArrayList 的效率要高于LinkedList。这是因为 ArrayList 在添加元素到尾部的时候,不需要复制重排数据,效率非常高。而 LinkedList 虽然也不用循环查找元素,但 LinkedList 中多了 new 对象以及变换指针指指向对象的过程,所以效率要低于 ArrayList。 说明一下,这里我是基于 ArrayList 初始化容量足够,排除动态扩容数组容量的情况下进行的测试,如果有动态扩容的情况,ArrayList 的效率也会降低。 从集合头部位置删除元素 ArrayList>LinkedList 从集合中间位置删除元素 ArrayList 从集合尾部位置删除元素 ArrayList for(;;) 循环 ArrayList 迭代器迭代循环 ArrayList≈LinkedList 我们可以看到,LinkedList 的 for 循环性能是最差的,而 ArrayList 的 for 循环性能是最好的。这是因为 LinkedList 基于链表实现的,在使用 for 循环的时候,每一次 for 循环都会去遍历半个List,所以严重影响了遍历的效率;ArrayList 则是基于数组实现的,并且实现了RandomAccess 接口标志,意味着 ArrayList 可以实现快速随机访问,所以 for 循环效率非常高。 LinkedList 的迭代循环遍历和 ArrayList 的迭代循环遍历性能相当,也不会太差,所以在遍历 LinkedList 时,我们要切忌使用 for循环遍历。2.ArrayList 和 LinkedList 删除元素操作测试
3.ArrayList 和 LinkedList 遍历元素操作测试