一、前言
站在巨人的肩膀上,本系列的Java集合框架文章参考了skywang12345——Java 集合系列,真心讲的很好,可以参考。但是不足的是,时间过于长久了,大佬的文章是基于JDK1.6.0_45,对于现在来说至少都用JDK 8.0以上了,而且JDK 6.0与JDK 8.0中集合框架的改动挺大的,所以本系列的文章是基于JDK_1.8.0_161进行说明的。
二、List概括
前面的文章解析了List的全部内容(ArrayList, LinkedList, Vector, Stack):
那我们先来回顾一下List的框架图:
- List是一个接口,它继承于Collection的接口。它代表着有序的队列。
- AbstractList是一个抽象类,它继承于AbstractCollection。AbstractList实现List接口中除size()、get(int location)之外的函数。
- AbstractSequentialList是一个抽象类,它继承于AbstractList。AbstractSequentialList 实现了“链表中,根据index索引值操作链表的全部函数”。
- ArrayList,LinkedList,Vector,Stack是List的4个实现类。
- ArrayList是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。
- LinkedList是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率高。
- Vector是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。
- Stack是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。
三、List使用场景
如果涉及到“栈”、“队列”、“链表”等操作,应该考虑用List,具体的选择哪个List,根据下面的标准来取舍。
1、对于需要快速插入,删除元素,应该使用LinkedList。
2、对于需要快速随机访问元素,应该使用ArrayList。
3、对于单线程环境,如果List仅仅只会被单个线程操作,此时应该使用非同步的类(如ArrayList)。
4、对于多线程环境,且List可能同时被多个线程操作,此时,应该使用同步的类(如Vector)。
下面通过一个具体的例子,来验证上面的(1)和(2)结论。代码如下:
public class Main {
private static final int COUNT = 100000;
private static LinkedList<Integer> linkedList = new LinkedList<>();
private static ArrayList<Integer> arrayList = new ArrayList<>();
private static Vector<Integer> vector = new Vector<>();
private static Stack<Integer> stack = new Stack<>();
public static void main(String[] args) {
// 插入
insertByPosition(stack);
insertByPosition(vector);
insertByPosition(linkedList);
insertByPosition(arrayList);
System.out.println();
// 随机读取
readByPosition(stack);
readByPosition(vector);
readByPosition(linkedList);
readByPosition(arrayList);
System.out.println();
// 删除
deleteByPosition(stack);
deleteByPosition(vector);
deleteByPosition(linkedList);
deleteByPosition(arrayList);
}
// 获取list的名称
private static String getListName(List<Integer> list) {
if (list instanceof LinkedList) {
return "LinkedList";
} else if (list instanceof ArrayList) {
return "ArrayList";
} else if (list instanceof Stack) {
return "Stack";
} else if (list instanceof Vector) {
return "Vector";
} else {
return "List";
}
}
// 向list的指定位置插入COUNT个元素,并统计时间
private static void insertByPosition(List<Integer> list) {
long startTime = System.currentTimeMillis();
// 向list的位置0插入COUNT个数
for (int i = 0; i < COUNT; i++) {
list.add(0, i);
}
long endTime = System.currentTimeMillis();
long interval = endTime - startTime;
System.out.println(getListName(list) + " : insert " + COUNT + " elements into the 1st position use time:" + interval + " ms");
}
// 从list的指定位置删除COUNT个元素,并统计时间
private static void deleteByPosition(List<Integer> list) {
long startTime = System.currentTimeMillis();
// 删除list第一个位置元素
for (int i = 0; i < COUNT; i++) {
list.remove(0);
}
long endTime = System.currentTimeMillis();
long interval = endTime - startTime;
System.out.println(getListName(list) + " : delete " + COUNT + " elements from the 1st position use time:" + interval + " ms");
}
// 根据position,不断从list中读取元素,并统计时间
private static void readByPosition(List<Integer> list) {
long startTime = System.currentTimeMillis();
// 读取list元素
for (int i = 0; i < COUNT; i++) {
list.get(i);
}
long endTime = System.currentTimeMillis();
long interval = endTime - startTime;
System.out.println(getListName(list) + " : read " + COUNT + " elements by position use time:" + interval + " ms");
}
}
运行结果如下:
Stack : insert 100000 elements into the 1st position use time:420 ms
Vector : insert 100000 elements into the 1st position use time:433 ms
LinkedList : insert 100000 elements into the 1st position use time:7 ms
ArrayList : insert 100000 elements into the 1st position use time:425 ms
Stack : read 100000 elements by position use time:4 ms
Vector : read 100000 elements by position use time:3 ms
LinkedList : read 100000 elements by position use time:4327 ms
ArrayList : read 100000 elements by position use time:1 ms
Stack : delete 100000 elements from the 1st position use time:436 ms
Vector : delete 100000 elements from the 1st position use time:422 ms
LinkedList : delete 100000 elements from the 1st position use time:4 ms
ArrayList : delete 100000 elements from the 1st position use time:431 ms
根据输出结果,我们可以发现:
插入10万个元素,LinkedList所花时间最短:7ms。
删除10万个元素,LinkedList所花时间最短:4ms。
遍历10万个元素,LinkedList所花时间最长:4327ms;而ArrayList、Stack和Vector则相差不多,都只用了几毫秒。
考虑到Vector是支持同步的,而Stack又是继承于Vector的;因此,得出结论:
1、对于需要快速插入,删除元素,应该使用LinkedList。
2、对于需要快速随机访问元素,应该使用ArrayList。
3、对于“单线程环境” 或者 “多线程环境,但List仅仅只会被单个线程操作”,此时应该使用非同步的类。
四、LinkedList和ArrayList性能差异分析
下面我们看看为什么LinkedList中插入元素很快,而ArrayList中插入元素很慢!先来看看LinkedList向指定位置插入元素的方法:add(int index, E element)。
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
//如果索引等于长度,则直接添加到列表末尾
linkLast(element);
else
linkBefore(element, node(index));
}
//根据索引下标,返回对应的元素值
Node<E> node(int 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;
}
}
// 将节点(节点数据是e)添加到succ节点之前。
void linkBefore(E e, Node<E> succ) {
//获取succ的前节点
final Node<E> pred = succ.prev;
//创建一个新节点,并把新节点的前节点设置成succ的前节点,把新节点的后节点设置成succ节点
final Node<E> newNode = new Node<>(pred, e, succ);
//把succ的前节点设置成新节点
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
从中,我们可以看出:通过add(int index, E element)向LinkedList插入元素时。先是在双向链表中找到要插入节点的位置index;找到之后,再插入一个新节点。双向链表查找index位置的节点时,有一个加速动作:若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。
接着,我们看看ArrayList.java中向指定位置插入元素的代码。如下:
// 将element添加到ArrayList的指定位置
public void add(int index, E element) {
rangeCheckForAdd(index);
//确定容量是否充足
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,size - index);
elementData[index] = element;
size++;
}
ensureCapacityInternal(size + 1)的作用是:确认ArrayList的容量,若容量不够,则增加容量。真正耗时的操作是:System.arraycopy(elementData, index, elementData, index + 1,size - index)这句代码。
public static native void arraycopy(Object src, int srcPos,Object dest, int destPos,int length);
由于arraycopy方法是个JNI函数,看不到其中的源码,想要继续了解System.arraycopy源码的可以自己去找找。这就意味着,ArrayList的add(int index, E element)函数,会引起index之后所有元素移动!
通过上面的分析,我们就能理解为什么LinkedList中插入元素很快,而ArrayList中插入元素很慢。是由于LinkedList插入元素只需要改变节点的前后节点的指向,无需对数组数据进行移动。删除元素与插入元素的原理类似,这里就不再过多说明。
接下来,我们看看 “为什么LinkedList中随机访问很慢,而ArrayList中随机访问很快”。先看看LinkedList随机访问的代码:
//根据特定的索引,返回元素值
public E get(int index) {
//检查下标是否合法
checkElementIndex(index);
//返回节点的元素值
return node(index).item;
}
//根据下标返回元素节点
Node<E> node(int 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;
}
}
从中,我们可以看出:通过get(int index)获取LinkedList第index个元素时。先是在双向链表中找到要index位置的元素;找到之后再返回。双向链表查找index位置的节点时,有一个加速动作:若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。
下面看看ArrayList随机访问的代码:
//根据特定的索引,返回元素值
public E get(int index) {
//检查下标是否合法
rangeCheck(index);
return elementData(index);
}
//返回数组中index索引的值
E elementData(int index) {
return (E) elementData[index];
}
从中,我们可以看出:通过get(int index)获取ArrayList第index个元素时。直接返回数组中index位置的元素,而不需要像LinkedList一样进行遍历查找。
五、Vector和ArrayList比较
相同的地方:
- 它们都是List。它们都继承于AbstractList,并且实现List接口。
- 它们都实现了RandomAccess、Cloneable、Serializable接口。实现RandomAccess接口,意味着它们都支持快速随机访问;实现Cloneable接口,意味着它们能克隆自己;实现了Serializable接口,意味着它们能进行序列化传输。
- 它们都是通过数组实现的,本质上都是动态数组。
- 它们数组的初始容量都是10。只不过Vector是在构造方法中,而ArrayList是在第一次添加元素的时候,当然前提都是调用无参数的构造方法。
- 它们都支持Iterator和listIterator遍历。它们都继承于AbstractList,而AbstractList中分别实现了iterator()接口返回Iterator迭代器和listIterator()返回ListIterator迭代器。
不同的地方:
- 线程安全性不一样。ArrayList是非线程安全的;而Vector是线程安全的,它的函数都是synchronized的,即都是支持同步的。ArrayList适用于单线程,Vector适用于多线程。
- 构造方法个数不同。Vector有4个构造方法,ArrayList有3个。
- 容量增加方式不同。当ArrayList容量不足时,新容量=旧容量+旧容量/2,即扩容为原来的1.5倍;当Vector容量不足时分2种情况,一种是调用了构造方法时传入了容量增量,那么新容量=旧容量+容量增量,一种是调用无参的构造方法或者其他构造方法时,新容量=旧容量+旧容量,即扩容为原来的2倍。
- 对Enumeration的支持不同。Vector支持通过Enumeration去遍历,而List不支持。