前言
在面试的时候,经常会被问到几个问题:
ArrayList和LinkedList的区别,相信大部分朋友都能回答上:
ArrayList是基于数组实现,LinkedList是基于链表实现
当随机访问List时,ArrayList比LinkedList的效率更高,等等
当被问到ArrayList和LinkedList的使用场景是什么时,大部分朋友的答案可能是:
ArrayList和LinkedList在新增、删除元素时,LinkedList的效率要高于 ArrayList,而在遍历的时候,ArrayList的效率要高于LinkedList
那这个回答是否准确呢?今天我们就来研究研究!
我们先来简单介绍下ArrayList和LinkedList的原理实现!
源码分析
ArrayList
实现类
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList实现了List接口,继承了AbstractList抽象类,底层是数组实现的,并且实现了自增扩容数组大小。
ArrayList还实现了Cloneable接口和Serializable接口,所以他可以实现克隆和序列化。
ArrayList还实现了RandomAccess接口,这个接口是一个标志接口,他标志着“只要实现该接口的List类,都能实现快速随机访问”。
基本属性
ArrayList属性主要由数组长度size、对象数组elementData、初始化容量default_capacity等组成, 其中初始化容量默认大小为10。
//默认初始化容量 private static final int DEFAULT_CAPACITY = 10; //对象数组 transient Object[] elementData; //数组长度 private int size;
从ArrayList属性来看,elementData被关键字transient修饰了,transient关键字修饰该字段则表示该属性不会被序列化。
但ArrayList其实是实现了序列化接口,这是为什么呢?
由于ArrayList的数组是基于动态扩增的,所以并不是所有被分配的内存空间都存储了数据。
如果采用外部序列化法实现数组的序列化,会序列化整个数组,ArrayList为了避免这些没有存储数据的内存空间被序列化,内部提供了两个私有方法writeObject以及readObject来自我完成序列化与反序列化,从而在序列化与反序列化数组时节省了空间和时间。
因此使用transient修饰数组,是防止对象数组被其他外部方法序列化。
ArrayList自定义序列化方法如下:
初始化
有三种初始化办法:无参数直接初始化、指定大小初始化、指定初始数据初始化,源码如下:
当ArrayList新增元素时,如果所存储的元素已经超过其已有大小,它会计算元素大小后再进行动态扩容,数组的扩容会导致整个数组进行一次内存复制。
因此,我们在初始化ArrayList时,可以通过第一个构造函数合理指定数组初始大小,这样有助于减少数组的扩容次数,从而提高系统性能。
注意点:
ArrayList 无参构造器初始化时,默认大小是空数组,并不是大家常说的 10,10 是在第一次 add 的时候扩容的数组值。
新增元素
ArrayList新增元素的方法有两种,一种是直接将元素加到数组的末尾,另外一种是添加元素到任意位置。
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
两个方法的相同之处是在添加元素之前,都会先确认容量大小,如果容量够大,就不用进行扩容;如果容量不够大,就会按照原来数组的1.5倍大小进行扩容,在扩容之后需要将数组复制到新分配的内存地址。
下面是具体的源码:
这两个方法也有不同之处,添加元素到任意位置,会导致在该位置后的所有元素都需要重新排列,而将元素添加到数组的末尾,在没有发生扩容的前提下,是不会有元素复制排序过程的。
所以ArrayList在大量新增元素的场景下效率不一定就很慢的
如果我们在初始化时就比较清楚存储数据的大小,就可以在ArrayList初始化时指定数组容量大小,并且在添加元素时,只在数组末尾添加元素,那么ArrayList在大量新增元素的场景下,性能并不会变差,反而比其他List集合的性能要好。
删除元素
ArrayList 删除元素有很多种方式,比如根据数组索引删除、根据值删除或批量删除等等,原理和思路都差不多。
ArrayList在每一次有效的删除元素操作之后,都要进行数组的重组,并且删除的元素位置越靠前,数组重组的开销就越大。
我们选取根据值删除方式来进行源码说明:
遍历元素
由于ArrayList是基于数组实现的,所以在获取元素的时候是非常快捷的。
public E get(int index) { rangeCheck(index); return elementData(index); } E elementData(int index) { return (E) elementData[index]; }