ArrayList和LinkedList的实现差异
List代表一种线性表的数据结构,ArrayList则是一种顺序存储的线性表,ArrayList底层采用动态数组的形式保存每一个集合元素,LinkedList则是一种链式存储的线性表,其本质上就是一个双向链表,它不仅实现了List接口,还实现了Deque接口,Deque代表了一种双端队列,既具有队列(FIFO)的特性,也具有栈(FILO)的特性,也就是说,LinkedList既可以当成双向链表使用,也可以当成队列使用,还可以当成栈来使用。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
ArrayList底层采用一个elementData数组来保存所有集合元素,因此ArrayList在插入元素时需要完成两件事情:
①、保证ArrayList底层封装的数组长度大于集合元素的个数
②、将插入位置之后的所有数组元素“整体搬家”,向后移动一“格”
当删除ArrayList集合中指定位置的元素时,程序也要进行“整体搬家”,而且还需要将被删除索引处的数组元素赋值为null,下面是ArrayList集合的remove(int index)方法的源代码。
public E remove(int index) {
rangeCheck(index); modCount++;
E oldValue = elementData(index); 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 return oldValue;
}
对于ArrayList集合而言,当程序向ArrayList中添加、删除集合元素时,ArrayList底层都是需要对数组进行“整体搬家”,因此性能很差。
但如果程序调用get(int index)方法取出ArrayList集合中的元素时,性能和数组几乎相同,效率非常快。下面是ArrayList集合的get(int index)方法的源代码。
E elementData(int index) {
return (E) elementData[index];
} public E get(int index) {
rangeCheck(index);
return elementData(index);
}
LinkedList本质上是一个双向链表,因此它使用内部类来保存每一个集合元素。
private static class Node<E> {
//集合元素
E item;
//保存指向下一个链表节点的引用
Node<E> next;
//保存指向上一个链表节点的引用
Node<E> prev;
//普通构造器
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
从上面程序中的粗体字代码可以看出,一个Node对象代表双向链表的一个节点,该对象中的next变量指向下一个节点,previous子指向上一个节点。
由于LinkedList采用双向链表来保存集合元素,因此它在添加集合元素时,只要对链表进行插入即可。下面是LinkedList添加节点的源码。
public void add(int index, E element) {
checkPositionIndex(index); if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
从上面代码看出,由于Linked本质上就是一个双向链表,因此它非常方便的在指定节点之前插入新节点。
上面add(int index,E element)方法实现中用到了以下三个方法。
①、node(int index):搜索指定索引出的元素
②、linkLast(E e):将e新节点插入到最后
③、linkBefore(E e,Node<E> succ):在succ节点之前插入e新节点
node(int index)实际上就是get(int index)方法的底层实现。Linked必须逐个元素的搜索,直到找到第index个元素为止。node(int index)采用二分法的方式进行查找,以下是该方法的源代码。
Node<E> node(int index) {
// assert isElementIndex(index);
//如果index小于size/2
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;
}
}
上面的node(int index)方法就是逐个元素的找到index索引处的元素,只是由于LinkedList是一个双向链表,因此程序先根据index的值判断它离链表头近还是离链表尾近,如果离链表头近则从头端开始搜索,如果离链表尾近则从尾端开始搜素。
Linked的get(int index)方法只是对上面node(int index)方法的简单包装。get(int index)方法的源代码如下。
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
但单纯的插入操作就比较简单了,只要修改几个节点里面的previous、next引用的值即可。下面是linkedbefore(E e, Node<E> succ)方法的源代码。
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
如果只是单纯的添加某个节点,那么LinkedList的性能是非常好的,但如果需要向某个指定索引处添加节点,LinkedList必须先找到指定索引处的节点,这个搜索过程的系统开销并不小,因此LinkedList的add(int index,E e)方法的性能并不是特别好。如果只使用LinkedList的addFrist(E e)、addLast(E e)、offerFrist(E e)、offerLast(E e)、pollFrist(E e)、pollLast(E e)的话,性能是非常好的,因为可以避免搜索过程。
类似的,LinkedList为了实现remove(int index)方法,也必须先通过node(int index)方法找到index索引处的节点,然后修改它前一个节点的next引用,以及后一个节点的previous引用,下面是该方法的源代码。
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
} 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;
}
综上所述,ArrayList、LinkedList有各自适用的场景,大部分情况下,ArrayList的性能总是优于Linkedlist,因此绝大部分都应该考虑使用ArrayList集合。但如果程序荆楚需要添加、删除元素,尤其是经常需要add(E e)方法向集合中添加元素,则应该考虑使用LinkedList集合了。