ArrayList和LinkedList都是List接口的实现类,都能用于存储一组有序可重复的数据。
ArrayList
ArrayList是由数组实现的,我们知道,数组是有长度概念的,创建数组对象的时候我们要为它设置长度,所以ArrayList也是有容量概念的,创建ArrayList对象的时候,我们要为它设置初始容量,不设置的话,它也会有一个默认的初始容量是10,这个初始容量会被设置为底层数组的长度。虽然数组的长度不可变不支持扩容,但ArrayList是支持扩容的,当ArrayList的容量不够用时,就会触发自动扩容,ArrayList是1.5倍扩容的,扩容时会创建一个新数组,新数组的容量为旧数组的1.5倍,将旧数组中的数据复制到新数组中之后,用新数组替换掉旧数组。
因为数组是静态分配内存的,它占用的是一块儿连续的内存区域,所以ArrayList占用的也是一块连续的内存区域,所以在通过索引检索元素【执行get(int index)方法】时,ArrayList能够基于内存首地址、所存储的元素的数据类型计算出给定索引对应的元素在内存中的存储地址【首地址 + (元素长度 * 索引值)】,也就是,ArrayList能够通过索引值快速定位到元素,所以ArrayList检索元素的效率非常高。
但是ArrayList在某个索引处插入和删除元素的效率比较低,也就是add(int index, Object element) 和 remove(int index)方法的执行效率比较低,因为插入和删除操作涉及到其他元素的移动。以插入元素为例:插入操作分两步执行,第一步是将指定元素插入到列表中的指定位置处,第二步是将此位置后续所有元素的索引值都加一,因此插入操作会导致插入位置后面的所有元素都往后移动一个位置,所以插入操作效率比较低,删除元素同理。
LinkedList
LinkedList底层是由链表实现的,链表中的各个节点不止存储元素,还要多存储两个引用,一个引用指向此节点的前驱节点,一个引用指向此节点的后继节点,链表是动态分配内存的,每次添加一个元素就为这个元素分配一块内存来存储,因此,链表没有容量的概念,链表所占用的内存也是不连续的,所以虽然LinkedList也有索引的概念,但是无法通过索引快速定位到元素的内存地址,LinkedList中的索引只是一个顺序概念,记录的是元素的插入顺序,通过索引值,只能够知道越小的索引越靠近链表头,越大的索引越靠近链表尾。那么LinkedList是如何检索元素的呢?
LinkedList的get(int index)方法是通过链表遍历来实现的,在遍历之前,会先有一个加速操作,这个加速操作是去比较两个值的大小:比较指定的index的值 和 size/2的大小,size是链表中元素的个数。当index < size/2时,索引更靠近链表头,所以从链表头部开始遍历;当index > size/2时,索引更靠近链表尾,所以从链表尾部开始遍历。无论从头部还是尾部开始遍历,检索元素都是通过遍历来实现的,所以,LinkedList检索元素的效率比较低。
但是LinkedList在某个索引处插入和删除元素的效率非常高,也就是add(int index, Object element) 和 remove(int index)方法的执行效率比较高,因为插入和删除操作对其他节点的影响非常小,只涉及到两个引用值的改变。以插入操作为例,插入操作也是分成两步来实现的,第一步是将指定元素插入到链表中的指定节点处,第二步是修改此节点的前驱节点所记录的后继节点引用的值,和修改此节点的后继节点所记录的前驱节点引用的值。对其他节点的影响非常小,只有这两个引用的改变,所以LinkedList的插入操作效率非常高,删除元素同理。
除了实现List接口外,LinkedList还实现了Deque接口,Deque是Queue接口的子接口,定义了一个双端队列数据结构,与单端队列的只能在一端入队另一端出队不同,双端队列的两端都是支持入队和出队操作的,因此双端队列可以用于实现栈。而LinkedList实现了Deque接口,所以LinkedList可以被用做一个双端队列,也可以被用于实现栈。
需要说明的一点是:将一个LinkedList对象声明为List类型和Queue类型是不同的,如果声明为Queue类型,是会窄化LinkedList的方法的使用的,这种情况下,LinkedList只能使用它属于Queue接口的那些方法。