这篇文章是来自知乎上的一个问题。
相信很多人在面试时都被问过这个问题,然后一般回答:ArrayList在指定下标访问时快,LinkedList在插入/删除元素时快。
其实这是一种人云亦云的谬误。可能最初有人这么回答,然后不加验证地转来转去,后面就成了八股文中的“标准答案”。
这个问题其实早在2015年,就由作者Joshua Bloch在Twitter上给出了回答:“有人真的使用过LinkedList吗?我写了它,但是我从没用过”。
以下是我在知乎上的回答。
——————————————————————————————————————————————————————————
先说结论:需要List的场合,绝大部分情况下选择ArrayList;需要Queue或Stack的场合,绝大部分情况下选择ArrayDeque。
看实际的性能测试。对ArrayList和LinkedList分别做100000次以下操作,统计执行时间(ms):
尾部插入 | 头部插入 | 中间插入 | 遍历 | |
---|---|---|---|---|
ArrayList | 5 | 346 | 152 | 7 |
LinkedList | 2 | 4 | 5044 | 2 |
逐条分析解释:
- 尾部插入:相差不大,都是O(1)的操作。
- 头部插入:LinkedList明显优于ArrayList。因为ArrayList每次操作都涉及底层全量数组的System.arraycopy();而LinkedList是双向链表,内存保存了headNode,所以是O(1)的操作。
- 中间插入:ArrayList明显优于LinkedList。因为LinkedList每次需要遍历寻找插入节点,仅这一步复杂度就是O(n)。
- 遍历:相差不大。
除了上述时间消耗的比较,在空间消耗上ArrayList也优于LinkedList。虽然ArrayList会扩容到1.5倍,但是LinkedList内部为每个存储元素封装了Node对象,除了Node对象头部,还要保存前后Node的地址和当前存储元素的地址,实际上LinkedList占用内存要远大于ArrayList。
结论:仅在头部插入/删除时,LinkedList在时间上有性能优势。
不过问题来了:既然你需要在头部频繁插入/删除,那么为什么不选择更合适的数据结构呢?比如Stack或者Queue。
那么LinkedList是实现这两种数据结构的最优选择吗?答案也不是,而是ArrayDeque。
不多说,再看性能测试对比。对ArrayDeque和LinkedList分别做1000000次以下操作,统计执行时间(ms):
头部插入 | 尾部插入 | 遍历 | |
---|---|---|---|
ArrayList | 12 | 126 | 6 |
LinkedList | 61 | 153 | 10 |
可以看出,ArrayDeque在每项时间指标都优于LinkedList。另外在空间上,ArrayDeque底层使用的循环数组也是优于LinkedList使用的Node链表。
结论:无论在时间还是空间上,ArrayDeque都优于LinkedList。
顺便说一句,JDK中还提供了Stack类。虽然它名字就叫Stack,一般不会用到它实现栈,甚至Java官方都建议不要使用它。因为有以下缺陷:
- 继承关系和方法设计混乱。
- 为了线程安全所有方法加线程锁,性能较差。
那么综合看来,LinkedList是否一无是处呢?我觉得它的存在还是有一定价值的:
- 典型的数据结构。作为一种基础数据结构,链表在几乎每种语言中都有自己的实现。LinkedList作为链表在Java中的实现,起到了教学和理论研究的作用。它在Java中性能不好更多与JVM本身的内存结构有关,并非在所有语言中都是这样。
- 历史原因。LinkedList刚出来时还是实现Queue和Stack的最佳选择,不过后面1.6版本推出的ArrayDeque在性能上后来居上,为了版本兼容的考虑LinkedList还是保留了下来。