这是我参与更文挑战的第1天,活动详情查看:更文挑战。 本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动连接。java
常常在面试时,被问到集合的概念,集合 List、Map、Set 等底层设计以及其使用场景与注意细节。但大部分人的回答都是千篇一概,跟网上的答案如出一辙,这是致命滴。其实,你们都错了,尤为是网上,更是误导你们,详细缘由,且听我来分析。面试
在广大的网友心中,List 是一个缓存数据的容器,是 JDK 为开发者提供的一种集合类型。面试时,被问到最多见的就是 ArrayList 和 LinkedList 的区别。编程
相信大部分网友都能回答上:ArrayList 是基于数组实现,LinkedList 是基于链表实现。而在使用场景时,我发现大部分网友的答案都是:在新增、删除操做时,LinkedList 的效率要高于 ArrayList,而在查询、遍历操做的时候,ArrayList 的效率要高于 LinkedList。这个答案是否准确呢?今天就带你们验证一哈。数组
首先,你们都知道 ArrayList、LinkedList 都继承了 AbstractList 抽象类,而 AbstractList 实现了 List 接口。ArrayList 使用数组实现,而 LinkedList 使用了双向链表实现。接下来,咱们就详细地分析下 ArrayList 和 LinkedList 的性能。缓存
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
复制代码
在源码中,咱们知道 ArrayList 除了实现克隆和序列化,还实现了 RandomAccess 接口。你们可能会对这个接口比较陌生,经过代码咱们能够发现,这个接口实际上是一个空接口,没有实现逻辑,那么 ArrayList 为何要实现它呢?原来 RandomAccess 接口是一个标志接口,它标志着只要实现该接口,就能实现快速随机访问。markdown
至于 ArrayList、LinkedList 的各类操做方法这里再也不说了,你们能够看 这一篇。并发
接下来,咱们看看一些测试数据,以测试 50000 次为例:dom
头部:ArrayList.Time 大于 LinkedList.Time
中间:ArrayList.Time 小于 LinkedList.Time
末尾:ArrayList.Time 小于 LinkedList.Time
复制代码
经过这测试,咱们能够看到 LinkedList 新增元素的未必要快于 ArrayList。oop
因为 ArrayList 是数组实现的,而数组是一块连续的内存空间,在新增元素到数组头部的时候,须要对头部之后的数据进行重排,因此效率很低。而 LinkedList 是基于链表实现,在新增元素的时候,首先会经过循环查找到新增元素的位置,若是要新增的位置处于前半段,就从前日后找;若其位置处于后半段,就从后往前找。故 LinkedList 新增元素到头部是很是高效的。post
在中间位置插入时,ArrayList 一样有部分数据须要重排,效率也不是很高,而 LinkedList 将元素新增到中间,耗时最久的,由于靠近中间位置,在新增元素以前的循环查找是遍历元素最多的操做。
而在尾部操做时,发如今没有扩容的前提下,ArrayList 的效率要高于 LinkedList。这是由于 ArrayList 在新增元素到尾部的时候,不须要复制、重排,效率很是高。而 LinkedList 虽然也不用循环查找元素,但 LinkedList 中多了 new 对象以及变换指针指向对象的逻辑,因此要耗时多于 ArrayList 的操做。
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
复制代码
头部:ArrayList.Time 大于 LinkedList.Time
中间:ArrayList.Time 小于 LinkedList.Time
末尾:ArrayList.Time 小于 LinkedList.Time
复制代码
你们会发现 ArrayList 和 LinkedList 删除操做的测试结果和新增的结果很接近,这是同样的道理,我就不赘述了。
for循环:ArrayList.Time 小于 LinkedList.Time
迭代器:ArrayList.Time 几乎等于 LinkedList.Time
复制代码
咱们能够看到,LinkedList 的 for 循环遍历比不上 ArrayList 的 for 循环。这是由于 LinkedList 基于链表实现的,在使用 for 循环的时候,每一次 for 循环都会去遍历大半个 List,因此严重影响了遍历的效率。而 ArrayList 是基于数组实现的,而且实现了 RandomAccess 接口标志,意味着 ArrayList 能够实现快速随机访问,因此 for 循环很是快。LinkedList 的迭代遍历和 ArrayList 的迭代性能差很少,也不会太差,因此在遍历 LinkedList 时,咱们要使用迭代循环遍历。
在一次 ArrayList 删除操做的过程当中,有下面两种写法:
public static void removeA(ArrayList<String> l) {
for (String s : l){
if (s.equals("aaa")) {
l.remove(s);
}
}
}
复制代码
public static void removeB(ArrayList<String> l) {
Iterator<String> it = l.iterator();
while (it.hasNext()) {
String str = it.next();
if (str.equals("aaa")) {
it.remove();
}
}
}
复制代码
第一种写法错误,第二种是正确的,缘由是上面的两种写法都有用到 list 内部迭代器Iterator,即遍历时,ArrayList 内部建立了一个内部迭代器 iterator,在使用 next 方法来取下一个元素时,会使用 ArrayList 里保存的一个用来记录 list 修改次数的变量 modCount,与 iterator 保存了一个叫 expectedModCount 的表示指望的修改次数进行比较,若是不相等则会抛出一个叫 ConcurrentModificationException 的异常。且在 for 循环中调用 list 中的 remove 方法,会走到一个 fastRemove 方法,该方法不是 iterator 中的方法,而是 ArrayList 中的方法,在该方法只作了 modCount++,而没有同步到 expectedModCount。因此不一致就抛出了 ConcurrentModificationException 异常了。
下面是 ArrayList 本身的remove 方法:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
复制代码
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
复制代码
若是有看过阿里 Java 编程规范就知道,在集合中进行 remove 操做时,不要在 foreach 循环里进行元素的 remove/add 操做。remove 元素使用 Iterator 方式,若是并发操做,须要对 Iterator 对象加锁。