在Java
中使用频率最高的列表一定是ArrayList
和LinkedList
,它们分别使用了不同的数据结构,前者是数组,后者是链表。
如果学习过数据结构,我相信数组和链表的时间复杂度一定耳熟能详:
- 数组,随机访问时间复杂度是 O(1),插入是 O(n)
- 链表,随机访问时间复杂度是 O(n),插入是 O(1)
那么在 Java 中,应对插入多,访问少的场景,是否就应该使用LinkedList
呢?不如写一段代码测试下两者随机访问和插入的性能吧。
代码比较简单,就是对大小为 10 万的LinkedList
和ArrayList
进行随机访问和增加元素到随机位置,并使用 StopWatch
来测量执行需要的时间:
/**
* LinkedList随机访问
*/
private void linkedListGet() {
// 生成拥有100000个元素的 LinkedList
List<Integer> list = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toCollection(LinkedList::new));
// 从 (1,100000) 取一个随机数作为下标,访问list中的这个元素,并多次循环
IntStream.rangeClosed(1, 100000).forEach(i -> list.get(ThreadLocalRandom.current().nextInt(100000)));
}
/**
* ArrayList随机访问
*/
private void arrayListGet() {
List<Integer> list = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toCollection(ArrayList::new));
IntStream.rangeClosed(1, 100000).forEach(i -> list.get(ThreadLocalRandom.current().nextInt(100000)));
}
/**
* LinkedList随机插入
*/
private void linkedListAdd() {
List<Integer> list = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toCollection(LinkedList::new));
IntStream.rangeClosed(1, 100000).forEach(i -> list.add(ThreadLocalRandom.current().nextInt(100000), 1));
}
/**
* ArrayList随机插入
*/
private void arrayListAdd() {
List<Integer> list = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toCollection(ArrayList::new));
IntStream.rangeClosed(1, 100000).forEach(i -> list.add(ThreadLocalRandom.current().nextInt(100000), 1));
}
@Test
public void test03() {
StopWatch stopWatch = new StopWatch();
stopWatch.start("linkedListGet");
linkedListGet();
stopWatch.stop();
stopWatch.start("arrayListGet");
arrayListGet();
stopWatch.stop();
log.info("随机访问速度对比:{}", stopWatch.prettyPrint());
StopWatch stopWatch2 = new StopWatch();
stopWatch2.start("linkedListAdd");
linkedListAdd();
stopWatch2.stop();
stopWatch2.start("arrayListAdd");
arrayListAdd();
stopWatch2.stop();
log.info("随机插入速度对比:{}", stopWatch2.prettyPrint());
}
结果非常 amazing:
15:33:28.311 [main] INFO com.example.util.StopWatchTest - 随机访问速度对比:StopWatch '': running time (millis) = 5177
-----------------------------------------
ms % Task name
-----------------------------------------
05157 100% linkedListGet
00020 000% arrayListGet
15:33:54.868 [main] INFO com.example.util.StopWatchTest - 随机插入速度对比:StopWatch '': running time (millis) = 26554
-----------------------------------------
ms % Task name
-----------------------------------------
25326 095% linkedListAdd
01228 005% arrayListAdd
随机访问和预期一样,使用数组作为底层数据结构的 ArrayList 拥有压倒性的优势,耗时只有 20 毫秒。
但是随机插入也是 ArrayList 占优,它只要 1.28 秒,LinkedList 居然需要 25.3 秒。
不是说好的链表
插入更快吗?这咋回事?
翻了 LinkedList 源码可以发现,插入时间复杂度是 O(1) 的前提是,已经有了那个要插入节点的指针。
但是怎么才能获得那个节点的Node
?还不是得循环嘛,循环也是需要时间的。
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
既然都到这儿了,我们不妨再测试下顺序插入,LinkedList 能否扳回一城?
@Test
public void testWrite() {
StopWatch stopWatch = new StopWatch();
List<Integer> arrayList = new ArrayList<>();
stopWatch.start("ArrayList");
IntStream.rangeClosed(1, 10000000).forEach(arrayList::add);
stopWatch.stop();
List<Integer> linkedList = new LinkedList<>();
stopWatch.start("LinkedList");
IntStream.rangeClosed(1, 10000000).forEach(linkedList::add);
stopWatch.stop();
log.info(stopWatch.prettyPrint());
}
可惜啊,ArrayList 依然遥遥领先:
16:00:27.385 [main] INFO com.example.util.StopWatchTest - StopWatch '': running time (millis) = 3334
-----------------------------------------
ms % Task name
-----------------------------------------
00687 021% ArrayList
02647 079% LinkedList
这回悬着的心终于死了,无论什么场景,ArrayList 都完胜 LinkedList。
毕竟 LinkedList 的亲爹 Josh Bloch 都发话了:正经人谁用 LinkedList?
本文由mdnice多平台发布
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。