一、前置技能:线性表
要想了解ArrayList和LinkedList,首先要知道线性表的概念。
- 线性表是最常用且最简单的一种数据结构,简单说,一个线性表是n个数据元素的有限序列。
- 线性表有两种存储结构:顺序存储结构和链式存储结构
- 顺序存储结构的特点是逻辑关系上相邻的两个元素在物理位置上也相邻。
- 优点:可以随机存取
- 缺点:插入/删除需要移动大量元素
- 链式存储结构的特点是不要求逻辑上相邻的元素在物理位置上也相邻。
- 优点:插入/删除无需移动大量元素
- 缺点:不可随机存取
- 链式又可以分为单链表(一个指向后继的指针域)、循环链表(表中最后一个结点的指针域指向头结点)、双向链表(两个指针域,一个指向后继,一个指向前驱)
- 顺序存储结构的特点是逻辑关系上相邻的两个元素在物理位置上也相邻。
而ArrayList就是Java中对线性表的顺序存储结构的实现(网上也有很多称为可增长数组的实现,意思都是一样的,可增长数组就相当于顺序表),LinkedList是对链式存储结构的实现,并且是双链表的实现。
因此自然而然的,ArrayList和LinkedList也就具有相应的线性表的特点,即ArrayList方便随机存取元素,但是插入/删除等操作较慢,而LinkedList则相反。
二、 java.util.ArrayList & java.util.LinkedList
- ArraylList类提供了List ADT的一种可增长数组的实现,或者说是线性表顺序存储结构的实现。
- 优点:对get和set的调用花费常数时间
- 缺点:插入新值和删除旧值得代价昂贵(除非变动是在末端进行,注意是末端,头端是不行的)
从定义我们可以看到ArrayList继承了List和RandomAccess接口,说明它实现了线性表和支持随机访问
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
......
}
ArrayList和Vector很像,不过ArrayList不是同步的,如果要求线程安全必须由程序员在外部保证。
要注意一下ArrayList含有fail-fast机制,Java.util包中的所有集合都是fail-fast的,这是Java集合的一种错误检测机制。在并发环境下,如果有一个线程正在通过iterator/listIterator遍历ArrayList时,有另一个线程修改了ArrayList里的值,那么这个迭代器会抛出ConcurrentModificationException异常,但无法保证这个异常一定会被抛出,所以这种fail-fast机制应当只能应用于检测bug时。
至于这个异常具体是如何被抛出的,请移步这篇博客:Java ConcurrentModificationException异常剖析
java.util.LinkedLst
- LinkedList类提供了List ADT的双链表实现
- 优点:插入新值和删除旧值开销小(若变动项的位置已知)
- 缺点:不容易作索引,因此get的调用昂贵(除非在两端附近调用,比如想get表后部的某项,那么搜索可以先从表的后部搜)。
三、举几个例子
栗子1:
如果每次add都是从末端进行,则无论是ArrayList还是LinkedList都是花费常数时间(忽略ArrayList偶尔进行的扩展)
public class Test {
private static final int N = 100000;
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
makeArrayList(list1, N);
List<Integer> list2 = new LinkedList<>();
makeLinkedList(list2, N);
}
private static void makeArrayList(List<Integer> list, int N) {
list.clear();
// 逐个从末端添加项来构造List
for (int i = 0; i < N; i++)
list.add(i);
}
private static void makeLinkedList(List<Integer> list, int N) {
list.clear();
// 逐个从末端添加项来构造List
for (int i = 0; i < N; i++)
list.add(i);
}
}
运行时间大概是:
ArrayList:17ms
LinkedList:16ms
栗子2:
如果每次add都是从前端进行,则对于LinkedList运行时间为O(N),但是对于ArrayList是O(N^2),因为对于ArrayList在前端进行add是一个O(N)操作
private static void makeArrayList(List<Integer> list, int N) {
list.clear();
// 逐个从前端添加项来构造List
for (int i = 0; i < N; i++)
list.add(0, i);
}
private static void makeLinkedList(List<Integer> list, int N) {
list.clear();
// 逐个从前端添加项来构造List
for (int i = 0; i < N; i++)
list.add(0, i);
}
运行时间大概是:
ArrayList:4022ms
LinkedList:32ms
栗子3:
如果需要多次使用get操作,比如计算一个List中数的和,对于ArrayList运行时间是O(N),但是对于LinkedList来说运行时间是O(N^2),因为在LinkedList中,对get的调用的运行时间是O(N)
public static int sum(List<Integer> list) {
int sum = 0;
for (int i = 0; i < N; i++)
sum += list.get(i);
return sum;
}
运行时间大概是:
ArrayList:124ms
LinkedList:12532ms
栗子:remove方法对LinkedList类的使用
再举个例子,现在我们需要将一个表中所有具有偶数值的项删除,那么,该怎么做呢?
一种想法是,构造一个新表,这个新表包含所有奇数,然后清除原表,把新表的值拷贝到原表中。
但是我们更希望直接在原表操作,写一个避免拷贝的表。
那么,我们应该使用ArrayList还是LinkedList呢?
显然,首先排除掉ArrayList,因为对于ArrayList几乎任意地方的删除都是昂贵的操作。
如果我们使用下面这种方式(先get到元素,再删除)
public static void removeElement(List<Integer> list) {
int i = 0;
while (i < list.size()) {
if (list.get(i) % 2 == 0)
list.remove(i);
else
i++;
}
}
这种方式对于两种表来说都是O(N^2)的,因为LinkedList对get的调用效率不高,而ArrayList对remove的调用效率不高。
换一种思路:使用迭代器一步步遍历表,这是高效的,但是!
public static void removeElement(List<Integer> list) {
for (Integer x : list) {
if (x % 2 == 0)
list.remove(x);
}
}
会抛出异常
Exception in thread "main" java.util.ConcurrentModificationException
我们前面提到过:在直接使用Iterator(不是通过增强for循环间接使用)时,要记住一个基本法则:如果对正在被迭代的集合进行结构上的改变(如add、remove、clear),那么迭代器就不再合法(会抛出ConcurrentModificationException)。
我们在此处,就是对正在被迭代的集合list进行remove操作,所以迭代器就不再合法,抛出ConcurrentModificationException。
那么,成功的做法是什么呢?
public static void removeElement(List<Integer> list) {
Iterator<Integer> itr = list.iterator();
while (itr.hasNext()) {
if (itr.next() % 2 == 0)
itr.remove();
}
}
在前面的Iterator类中,我们有解释过该类中的remove方法:可以删除由next最新返回的项。因此,我们想到,可以在迭代器找到一个偶数值后,使用该迭代器删除这个它刚找到的值!
对于LinkedList,对该迭代器的remove方法只需花费常数时间,因为该迭代器总是位于或在需要删除的节点的附近呀!因此,对于LinkedList,整个程序只需花费线性时间!
不过对于ArrayList,即使迭代器在需要删除节点的附近,其remove方法仍然昂贵,因为数组的项必须要移动!
那么,总结一下,什么时候用ArrayList或LinkedList呢?
- 使用ArrayList效率高的情况
- 需要频繁get/set,但是没有较多add/remove
- 使用LinkedList效率高的情况
- 需要频繁从表的前端add/remove,但是没有不需较多get