今天来看一下平时用的比较多的另外两种结构,ArrayList
和LinkedList
。ArrayList
是基于动态数组的结构,为什么说它是动态的呢?因为它是可以扩容的,怎么扩容我们后面再说。而LinkedList
是基于链表结构的。
ArrayList
还是老样子,先贴几个常量出来,后面遇到的话有个印象
// 默认容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组1
private static final Object[] EMPTY_ELEMENTDATA = {};
// 空数组2
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 数据
transient Object[] elementData; // non-private to
话不多说,先上图。
首先还是先new
一个ArrayList
, 那么我们先来看一下有哪些构造方法
还有一种构造方法用的不多,这里主要看这两个。一个是指定初始容量的,一个没有指定初始容量。很简单,指定初始容量的就直接创建对应容量大小的数组即可,如果确定容量的话,建议使用该构造方法,不然后面扩容很费时间。第二种就是不指定初始容量的,可以看到它仅仅是给this.elementData
赋了一个空数组,并没有分配空间。咦 ,有点奇怪,没有分配空间,指定容量,我们怎么添加数据。别急,我们继续往下看。
我们首先add
了数据1,在add
方法中,调用了ensureCapacityInternal
方法, 又在ensureCapacityInternal
中先后调用了calculateCapcity
和ensureExplicitCapacity
方法,顺序按照上图的1
、2
、3
。下面简单说一下这三个函数的作用。
calculateCapcity
是返回最小满足要求的最小容量的。这里我们看到了前面提过的DEFAULTCAPACITY_EMPTY_ELEMENTDATA
常量,它是一个默认容量的空数组,在我们没有指定初始容量时返回的,在该方法中就会对我们是否指定了初始容量进行一个判断。
紧接着执行了ensureExplicitCapacity
,这个方法本身并没有什么,重要的是在它内部调用了一个grow
方法,它就是ArrayList
的扩容机制。 看下它是怎么扩容的
重要的其实只有两部分,上面我标1
的就是扩容后的新容量,也就是旧容量的1.5
倍。确定了新容量大小之后呢,就在内存中寻找一段满足要求的、连续的内存,把旧数据数组复制过去,添加新的内容,回收旧的数组空间。这是过程很耗时。
ArrayList
的大致内容就是这些了,还有一些什么set
、remove
方法,基本思想都差不多,有兴趣的可以去看看源码。 下面简单介绍一下LinkedList
LinkedList
LinkedList
本质就是一个链表,无论是操作还是性能,都与我们平常所说的差不多,需要注意的是:它的底层是一个双向链表
每个Node
对象中不仅当前节点的元素,还包含LinkedList
中它前一个和后一个的元素,每添加一个元素,就要创建一个Node
对象。
前面说了这么多内容,那么ArrayList
和LinkedList
到底有什么区别呢?它们又分别适用于哪些应用场景呢?
总结
ArrayList
是基于数组的,因此访问操作,它的性能要优于LinkedList
,直接通过索引即可;而LinkedList
则要一个个节点遍历,直到找到对应的数据。但是在中间添加或删除元素它的时间复杂度为O(n)
,因为这要将所操作元素后面的元素整体往后挪动或整体往前挪动;而LinkedList
是基于链表的,只需要改动节点的指向即可,复杂度为O(1)
。对于在结尾添加或删除,两者的性能是相同的。- 对于
ArrayList
来说,它的空间浪费主要体现在列表的末尾留有一定的容量空间;而LinkedList
则体现在它每创建一个元素,都要耗费一定的空间。 ArrayList
应用在查询较多,但插入和删除比较少的场景中;LinkedList
主要用在查询少,而插入和删除较多的场景。