经过源码分析以后,小伙伴们是不是在想:“好像 ArrayList 在新增元素的时候效率并不一定比 LinkedList 低啊!”
当两者的起始长度是一样的情况下:
如果是从集合的头部新增元素,ArrayList 花费的时间应该比 LinkedList 多,因为需要对头部以后的元素进行复制。
public class ArrayListTest { public static void addFromHeaderTest(int num) { ArrayList<String> list = new ArrayList<String>(num); int i = 0; long timeStart = System.currentTimeMillis(); while (i < num) { list.add(0, i + "沉默王二"); i++; } long timeEnd = System.currentTimeMillis(); System.out.println("ArrayList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart)); } } /** * @author 微信搜「沉默王二」,回复关键字 PDF */ public class LinkedListTest { public static void addFromHeaderTest(int num) { LinkedList<String> list = new LinkedList<String>(); int i = 0; long timeStart = System.currentTimeMillis(); while (i < num) { list.addFirst(i + "沉默王二"); i++; } long timeEnd = System.currentTimeMillis(); System.out.println("LinkedList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart)); } }
num 为 10000,代码实测后的时间如下所示:
ArrayList从集合头部位置新增元素花费的时间595
LinkedList从集合头部位置新增元素花费的时间15
ArrayList 花费的时间比 LinkedList 要多很多。
如果是从集合的中间位置新增元素,ArrayList 花费的时间搞不好要比 LinkedList 少,因为 LinkedList 需要遍历。
public class ArrayListTest { public static void addFromMidTest(int num) { ArrayList<String> list = new ArrayList<String>(num); int i = 0; long timeStart = System.currentTimeMillis(); while (i < num) { int temp = list.size(); list.add(temp / 2 + "沉默王二"); i++; } long timeEnd = System.currentTimeMillis(); System.out.println("ArrayList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart)); } } public class LinkedListTest { public static void addFromMidTest(int num) { LinkedList<String> list = new LinkedList<String>(); int i = 0; long timeStart = System.currentTimeMillis(); while (i < num) { int temp = list.size(); list.add(temp / 2, i + "沉默王二"); i++; } long timeEnd = System.currentTimeMillis(); System.out.println("LinkedList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart)); } }
num 为 10000,代码实测后的时间如下所示:
ArrayList从集合中间位置新增元素花费的时间1
LinkedList从集合中间位置新增元素花费的时间101
ArrayList 花费的时间比 LinkedList 要少很多很多。
如果是从集合的尾部新增元素,ArrayList 花费的时间应该比 LinkedList 少,因为数组是一段连续的内存空间,也不需要复制数组;而链表需要创建新的对象,前后引用也要重新排列。
public class ArrayListTest { public static void addFromTailTest(int num) { ArrayList<String> list = new ArrayList<String>(num); int i = 0; long timeStart = System.currentTimeMillis(); while (i < num) { list.add(i + "沉默王二"); i++; } long timeEnd = System.currentTimeMillis(); System.out.println("ArrayList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart)); } } public class LinkedListTest { public static void addFromTailTest(int num) { LinkedList<String> list = new LinkedList<String>(); int i = 0; long timeStart = System.currentTimeMillis(); while (i < num) { list.add(i + "沉默王二"); i++; } long timeEnd = System.currentTimeMillis(); System.out.println("LinkedList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart)); } }
num 为 10000,代码实测后的时间如下所示:
ArrayList从集合尾部位置新增元素花费的时间69
LinkedList从集合尾部位置新增元素花费的时间193
1
2
ArrayList 花费的时间比 LinkedList 要少一些。
这样的结论和预期的是不是不太相符?ArrayList 在添加元素的时候如果不涉及到扩容,性能在两种情况下(中间位置新增元素、尾部新增元素)比 LinkedList 好很多,只有头部新增元素的时候比 LinkedList 差,因为数组复制的原因。
当然了,如果涉及到数组扩容的话,ArrayList 的性能就没那么可观了,因为扩容的时候也要复制数组。
04、ArrayList 和 LinkedList 删除元素时究竟谁快?
1)ArrayList
ArrayList 删除元素的时候,有两种方式,一种是直接删除元素(remove(Object)),需要直先遍历数组,找到元素对应的索引;一种是按照索引删除元素(remove(int))。
public boolean remove(Object o) { final Object[] es = elementData; final int size = this.size; int i = 0; found: { if (o == null) { for (; i < size; i++) if (es[i] == null) break found; } else { for (; i < size; i++) if (o.equals(es[i])) break found; } return false; } fastRemove(es, i); return true; } public E remove(int index) { Objects.checkIndex(index, size); final Object[] es = elementData; @SuppressWarnings("unchecked") E oldValue = (E) es[index]; fastRemove(es, index); return oldValue; }
但从本质上讲,都是一样的,因为它们最后调用的都是 fastRemove(Object, int) 方法。
private void fastRemove(Object[] es, int i) { modCount++; final int newSize; if ((newSize = size - 1) > i) System.arraycopy(es, i + 1, es, i, newSize - i); es[size = newSize] = null; }
从源码可以看得出,只要删除的不是最后一个元素,都需要数组重组。删除的元素位置越靠前,代价就越大。
2)LinkedList
LinkedList 删除元素的时候,有四种常用的方式:
remove(int),删除指定位置上的元素
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
先检查索引,再调用 node(int) 方法( 前后半段遍历,和新增元素操作一样)找到节点 Node,然后调用 unlink(Node) 解除节点的前后引用,同时更新前节点的后引用和后节点的前引用:
E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; } remove(Object),直接删除元素 public boolean remove(Object o) { if (o == null) { for (LinkedList.Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (LinkedList.Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; } 也是先前后半段遍历,找到要删除的元素后调用 unlink(Node)。 removeFirst(),删除第一个节点 public E removeFirst() { final LinkedList.Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } private E unlinkFirst(LinkedList.Node<E> f) { // assert f == first && f != null; final E element = f.item; final LinkedList.Node<E> next = f.next; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; }
删除第一个节点就不需要遍历了,只需要把第二个节点更新为第一个节点即可。
removeLast(),删除最后一个节点
删除最后一个节点和删除第一个节点类似,只需要把倒数第二个节点更新为最后一个节点即可。
可以看得出,LinkedList 在删除比较靠前和比较靠后的元素时,非常高效,但如果删除的是中间位置的元素,效率就比较低了。
这里就不再做代码测试了,感兴趣的小伙伴可以自己试试,结果和新增元素保持一致:
从集合头部删除元素时,ArrayList 花费的时间比 LinkedList 多很多;
从集合中间位置删除元素时,ArrayList 花费的时间比 LinkedList 少很多;
从集合尾部删除元素时,ArrayList 花费的时间比 LinkedList 少一点。
我本地的统计结果如下所示,小伙伴们可以作为参考:
ArrayList从集合头部位置删除元素花费的时间380
LinkedList从集合头部位置删除元素花费的时间4
ArrayList从集合中间位置删除元素花费的时间381
LinkedList从集合中间位置删除元素花费的时间5922
ArrayList从集合尾部位置删除元素花费的时间8
LinkedList从集合尾部位置删除元素花费的时间12
05、ArrayList 和 LinkedList 遍历元素时究竟谁快?
1)ArrayList
遍历 ArrayList 找到某个元素的话,通常有两种形式:
get(int),根据索引找元素 public E get(int index) { Objects.checkIndex(index, size); return elementData(index); }
由于 ArrayList 是由数组实现的,所以根据索引找元素非常的快,一步到位。
indexOf(Object),根据元素找索引 public int indexOf(Object o) { return indexOfRange(o, 0, size); } int indexOfRange(Object o, int start, int end) { Object[] es = elementData; if (o == null) { for (int i = start; i < end; i++) { if (es[i] == null) { return i; } } } else { for (int i = start; i < end; i++) { if (o.equals(es[i])) { return i; } } } return -1; }
根据元素找索引的话,就需要遍历整个数组了,从头到尾依次找。