原始碼上分析ArrayList/LinkedList的效能
環境:
JDK8
概述:
ArrayList底層是一個數組,陣列有容量限制,超出限制時會增加50%容量,預設第一次插入元素時建立大小為10的陣列。
LinkedList底層維護一個Node的雙向連結串列
參考其內部類Node<E>
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; } }
原始碼分析:
add向末尾追加元素
ArrayList 核心是add之前判斷陣列容量,當陣列容量不足時,會進行擴容
public boolean add(E e) { // 確定ArrayList的容量大小 必要時進行擴容 // 注意:size + 1,保證資源空間不被浪費, //按當前情況,保證要存多少個元素,就只分配多少空間資源 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } /** * 私有方法:明確 ArrayList 的容量,提供給本類使用的方法 * - 用於內部優化,保證空間資源不被浪費:尤其在 add() 方法新增時起效 * @param minCapacity 指定的最小容量 */ private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private void ensureExplicitCapacity(int minCapacity) { // 將“修改統計數”+1,該變數主要是用來實現fail-fast機制的 modCount++; // 防止溢位程式碼:確保指定的最小容量 > 陣列緩衝區當前的長度 // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } /** * 私有方法:擴容,以確保 ArrayList 至少能儲存 minCapacity 個元素 * - 擴容計算:newCapacity = oldCapacity + (oldCapacity >> 1); 擴充當前容量的1.5倍 * @param minCapacity 指定的最小容量 */ private void grow(int minCapacity) { // overflow-conscious code //防止溢位程式碼 int oldCapacity = elementData.length; // 運算子 >> 是帶符號右移. 如 oldCapacity = 10,則 newCapacity = 10 + (10 >> 1) = 10 + 5 = 15 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) // 若 newCapacity 大於最大儲存容量,則進行大容量分配 newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
LinkedList 核心是在新建Node後會修改相鄰Node的next及prev
public void add(E e) { checkForComodification(); lastReturned = null; if (next == null) linkLast(e); else linkBefore(e, next); nextIndex++; expectedModCount++; } /** * 將其作為尾節點 * Links e as last element. */ void linkLast(E e) { //將新加節點的前驅指向last節點 final java.util.LinkedList.Node<E> l = last; //建立節點(前驅是last節點,元素為e,後繼為null節點) final java.util.LinkedList.Node<E> newNode = new java.util.LinkedList.Node<>(l, e, null); //將last節點修改為指向新節點 last = newNode; //判斷新節點的前節點(就是以前的last節點)是不是為空 if (l == null) //如果新節點的前節點為空, // 說明這個list集合是一個空集合,這個新加節點是新增的第一個節點, //將first節點修改為指向新節點 first = newNode; else //如果新節點的前節點不為空 //說明這個list集合有元素 //將前節點的後繼修改為新節點 l.next = newNode; size++; modCount++; } /** * //在指定節點前插入節點,節點succ不能為空 * Inserts element e before non-null Node succ. */ void linkBefore(E e, java.util.LinkedList.Node<E> succ) { // assert succ != null; final java.util.LinkedList.Node<E> pred = succ.prev;//取出指定節點的前驅節點 //新建一個以指定節點為後繼節點,指定節點的前驅節點為前驅節點的節點 final java.util.LinkedList.Node<E> newNode = new java.util.LinkedList.Node<>(pred, e, succ); //修改指定節點的前驅為新節點 succ.prev = newNode; if (pred == null) //如果指定節點為first節點 first = newNode; else //將指定節點的後節點指向新節點 pred.next = newNode; size++; modCount++; }效能比較:
ArrayList如果不考慮擴容的情況,效率和LinkedList不會有太大差別,當然一旦陣列容量不足須要擴容時,ArrayList會將old array copy至擴容後的new array,ArrayList的初始陣列大小為10,所以在大批量的add操作中,效能會比LinkedList差。
Add 向指定位置新增元素
ArrayList 通過arraycopy方法實現指定位置之後的元素向後順移一位
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++; }
LinkedList 和末位追加沒太大區別,同樣是修改相鄰Node的next及prev
public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } /** * Returns the (non-null) Node at the specified element index. */ java.util.LinkedList.Node<E> node(int index) { // assert isElementIndex(index); // 判斷位置在連結串列前半段或者是後半段 if (index < (size >> 1)) { java.util.LinkedList.Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { java.util.LinkedList.Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }效能比較:
一目瞭然,ArrayList每次新增元素後,須要對後面的元素依次順移一位,效率上和LinkedList沒法比
get元素
ArrayList直接按陣列下標訪問元素,效能上沒有多餘損耗
public E get(int index) { rangeCheck(index); return elementData(index); } E elementData(int index) { return (E) elementData[index]; }
LinkedList 須要遍歷連結串列中的元素
public E get(int index) { checkElementIndex(index); return node(index).item; }
效能比較:由於ArrayList直接按陣列下標訪問元素,效能上沒有多餘損耗,在get元素上的效率比LinkedList高
remove元素
和Add一樣,ArrayList在remove(末位移除除外)之後須要對後面的元素下標進行變更
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; }
LinkedList 始終只是修改相鄰Node的next及prev
public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } E unlink(java.util.LinkedList.Node<E> x) { // assert x != null; final E element = x.item; final java.util.LinkedList.Node<E> next = x.next; final java.util.LinkedList.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還要copy一份新的陣列,效能上和LinkedList沒法比
總結:
ArrayList內部維護一個數組,末位追加元素和get元素的效能很客觀,但是在指定位置上進行新增移除操作上的效能較LinkedList差。
LinkedList是一個雙向連結串列,不管是新增抑或是移除,額外操作只是修改相鄰元素,效能強大,但是get元素須要遍歷獲取,效能上較ArrayList差。
ArrayList強在讀資料,LinkedList強在寫資料,如果集合僅需要讀取或者不涉及大批量元素的新增,以及不涉及大批量對中間元素的新增移除操作,建議使用ArrayList,其餘情況建議使用LinkedList。