ArrayList 与 LinkedList 的区别
ArrayList 与 LinkedList 均是常用的数据结构,但是二者也存在部分区别。
底层数据结构
ArrayList 的底层数据结构是数组,LinkedList 的数据结构是双向链表,这也就表示:ArrayList 所占的内存空间是连续的,LinkedList 是不连续的。
具体内存分布如下图所示:

随机访问速度(根据下标进行访问)
当使用下标访问获取元素时,由于 ArrayList 底层是数据,那么知道了第一个元素的地址,就很容易计算出后面若干个元素的地址,使用下标访问的速度极快。
但是由于 LinkedList 底层是双向链表,元素与元素之间的地址并不连续,没有规律,这时就需要从首元素开始,一个一个遍历,从而获取到指定下标的元素。
相比之下,根据下标进行访问时,ArrayList 的访问速度比 LinkedList 的访问速度更快。
若是查找某个元素是否存在,那么这两个数据结构的查询速度是一样的,均为 o(n)。
增删性能
头插
- 向 ArrayList 头部插入元素时,由于需要将 ArrayList 中所有的元素向后复制一次,所消耗的时间较多。
- 向 LinkedList 头部插入元素时,只需要新增一个节点,改变原来头结点与新节点的指针指向,所以头插性能较高。
尾插
向 ArrayList 最后一位插入元素时,只需要新增一个元素,不涉及到元素的复制,因此性能较高。(此处不讨论 ArrayList 的扩容情况)。
向 LinkedList 最后一位插入元素时,与其头插相同,性能较高。
此时可能会有疑问,LinkedList 进行尾插时,不需要遍历一遍找到尾部元素再进行插入错误吗?
针对上面的问题,源码中有解释:
/*** Pointer to first node.*/transient Node<E> first;/*** Pointer to last node.*/transient Node<E> last;其中,last 节点指向了 LinkedList 的最后一个元素,因此,在进行尾插时,只需要新增一个节点并改变 last 等指针的指向即可,性能较高。
中间插
向 ArrayList 中间插入元素时,由于涉及到元素的复制,性能就会下降,并且插入的位置越靠近头部,需要复制的元素就越多,性能下降的就越多。
向 LinkedList 中间插入元素时,需要先对 LinkedList 进行遍历,找到需要插入的位置,然后进行元素的插入,虽然不涉及元素的复制,但是遍历也需要消耗一定的时间,因此性能也较低。
内存占用
由于 ArrayList 的只需要存储元素本身,其内存占用较小。但是 LinkedList 不仅仅需要存储元素本身,还需要存储每个节点的 pre 指针与 next 指针,其占用的内存较大。
关于局部性原理的应用
什么是局部性原理
当我们需要计算 1 + 2 时,就需要 CPU 从内存中读取 1 和 2,但是虽然 CPU 读取内存中的数据很快,但是与计算速度相比,还是比较慢,这时就可以将 1 和 2 先存入 CPU 缓存中,这样 CPU 就可以直接从缓存中读取这两个数据,速度较快,计算完成后,将得到的结果写入缓存中,并在后面某个时间将结果写入内存中。
虽然在将 1 和 2 写入 CPU 缓存中,也会消耗一定的时间,但是由于后面可能还会使用到这两个数据,并且使用时只需要在 CPU 缓存中进行读取,就相当于只是第一次消耗的较多的时间,后面所消耗的时间就非常少了,可以提升计算效率。
于是,对于 ArrayList 就可以使用上面的局部性原理。
应用局部性原理
当我们访问 ArrayList 中的元素时,由于访问了第一个元素,就会有很大的可能访问后面几个元素,于是就可以将这几个元素一并存入 CPU 缓存中,这样下次访问时,就能节省一定的时间。并且由于 ArrayList 底层的数据结构是数组,内存分布连续,这样就不会浪费 CPU 缓存中的内存。
如下图所示:

当 CPU 缓存中的空间满时,就会淘汰掉最先加进来的元素。
但是,LinkedList 无法应用局部性原理,因为 LinkedList 的底层数据结构是双向链表,内存分布不连续,这样即使将一部分元素加入到 CPU 缓存中,也会浪费缓存中的空间,这样一来,CPU 缓存中只有少部分存入了有效元素,剩下的大部分空间均被浪费了。
如下图所示:

