List是java重要的數據結構之一,我們經常接觸到的有ArrayList、Vector和LinkedList三種,他們都繼承來自java.util.Collection介面,類圖如下 接下來,我們對比下這三種List的實現和不同: 一、基本實現 1、ArrayList和Vector使用了數組實現, ...
List是java重要的數據結構之一,我們經常接觸到的有ArrayList、Vector和LinkedList三種,他們都繼承來自java.util.Collection介面,類圖如下
接下來,我們對比下這三種List的實現和不同:
一、基本實現
1、ArrayList和Vector使用了數組實現,可以認為它們封裝了對內部數組的操作;它們兩個底層的實現基本可以認為是一致的,主要的一點區別在於對多線程的支持上面。ArrayList沒有對內部的方法做線程的同步,它不是線程安全的,而Vector內部做了線程同步,是線程安全的。
2、LinkedList使用了雙向鏈表數據結構,與基於數組實現的ArrayList和Vector相比,這是一種不同的實現方式,這也決定了他們不同的應用場景。LinkedList鏈表由一系列列表項構成,一個表項包含三個部分:元素內容、前驅表項和後驅表項,如下圖所示
在JDK的實現中,增加了兩個節點指針first、last分別指向首尾節點
二、不同之處
在這裡我們主要對比下ArrayList與LinkedList的不同之處
1、增加元素到列表尾端:
在ArrayList中增加元素到列表尾端
public boolean add(E e) { ensureCapacityInternal(size + 1); // 確保內部數組有足夠的空間 elementData[size++] = e; //將元素加入到數組的末尾,完成添加 return true; }
在這個過程當時,add的性能主要是由ensureCapacityInternal方法的實現,我們繼續往下跟蹤代碼
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
calculateCapacity方法會根據你對ArrayList初始化的不同,對當前elmentData這個對象數組進行非空判斷。如果它是一個空數組,則返回ArrayList預設容量和新容量比較的最大值,如果不為空則直接返回新容量。接下來在ensureExplicitCapacity方法中判斷,如果新容量大於當前對象數組的長度則調用grow方法對數組進行擴容。
在這裡我們可以看到如果ArrayList容量滿足需求時,add()其實就是直接對數組進行賦值,性能很高。而當ArraList容量無法滿足要求擴容時,需要對之前的數組進行複製操作。因此合理的數組大小有助於減少數組的擴容次數,如果使用時能夠預估ArrayList數組大小,併進行初始化,指定容量大小對性能會有所提升。
在LinkedList中增加元素到列表尾端
//尾端插入,即將節點值為e的節點設置為鏈表的尾節點 void linkLast(E e) { final Node<E> l = last; //構建一個前驅prev值為l,節點值為e,後驅next值為null的新節點newNode final Node<E> newNode = new Node<>(l, e, null); //將newNode作為尾節點 last = newNode; //如果原尾節點為null,即原鏈表為null,則鏈表首節點也設置為newNode if (l == null) first = newNode; else //否則,原尾節點的next設置為newNode l.next = newNode; size++; modCount++; }
LinkedList由於使用了鏈表結構,因此不需要維護容量的大小,這是相比ArrayList的優勢。但每次元素的增加都需要新建一個node對象,併進行更多的賦值操作。在大數據量頻繁的調用過程中,對性能會有所影響。
2、增加元素到任意位置:
void add(int index, E element)
由於實現上的不同,ArrayList和LinkedList在這個方法上存在存在一定的性能差異。由於ArrayList是基於數組實現的,而數組是一塊連續的記憶體空間,如果在數組的任意位置插入元素,必然導致在該位置後的所有元素需要重新排列,因此效率會比較低。
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++; }
可以看到,ArrayList每次插入操作,都會進行一次數組複製。並且插入的元素在List中位置越靠前,數組重組的開銷也越大。
再開LinkedList代碼實現
public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } Node<E> node(int index) { // assert isElementIndex(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; } } void linkBefore(E e, Node<E> succ) { // assert succ != null; //指定節點的前驅prev 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中定位一個節點需要遍歷鏈表,如果新增的位置處於List的前半段,則從前往後找;若其位置處於後半段,則從後往前找。因此指定操作元素的位置越靠前或這靠後,效率都是非常高效的。但如果位置越靠中間,需要遍歷半個List,效率較低。因此LinkedList中定位一個節點需要遍歷鏈表,所以下標有關的插入、刪除時間複雜度都變為O(n) ;
3、刪除任意位置元素
public E remove(int index)
對ArrayList來說,remove()方法和add()方法是相同的,在刪除指定位置元素後,都要對數組進行重組。代碼如下
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; }
可見,在進行一次有效刪除後,都要進行數組的重組。並且跟add()指定位置的元素一樣,刪除元素的位置越靠前,重組時的開銷就越大,刪除的元素位置越靠後,開銷越小
再看LinkedList中代碼的實現如下
public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } Node<E> node(int index) { // assert isElementIndex(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 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; }
可見跟之前的插入任意位置一樣,LinkedList中定位一個節點需要遍歷鏈表,效率跟刪除的元素的具體位置有關,所以刪除任意位置元素時間複雜度也為O(n) ;
4、隨機訪問
public E get(int index)
首先看ArrayList的實現代碼如下
public E get(int index) { rangeCheck(index); return elementData(index); } @SuppressWarnings("unchecked") E elementData(int index) { return (E) elementData[index]; }
可見ArrayList隨機訪問是直接讀取數組第幾個下標,效率很高。
LinkedList實現代碼如下
public E get(int index) { checkElementIndex(index); return node(index).item; } Node<E> node(int index) { // assert isElementIndex(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; } }
相反LinkedList隨機訪問,每次都需要遍歷半個List確定元素位置,效率較低。
5、總結
通過比較與分析ArrayList與LinkList兩種不同實現的的List的功能代碼後,我個人感覺兩種List的具體使用真的要看實際的業務場景,有些具體的功能如新增刪除等操作根據實際情況,效率不可一概而論。在這裡進行簡單的分析只是為了個人加強理解,如有不正確的地方還望指出與海涵。
參考資料:《Java程式性能優化》