java集合的實現細節--ArrayList和LinkedList
ArrayList和LinkedList的實現差異
List代表一種線性表的數據結構,ArrayList則是一種順序存儲的線性表,ArrayList底層采用動態數組的形式保存每一個集合元素,LinkedList則是一種鏈式存儲的線性表,其本質上就是一個雙向鏈表,它不僅實現了List接口,還實現了Deque接口,Deque代表了一種雙端隊列,既具有隊列(FIFO)的特性,也具有棧(FILO)的特性,也就是說,LinkedList既可以當成雙向鏈表使用,也可以當成隊列使用,還可以當成棧來使用。
public class LinkedList<E> extends AbstractSequentialList<E> implementsList<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集合了。
java集合的實現細節--ArrayList和LinkedList