1. 程式人生 > >原始碼上分析ArrayList/LinkedList的效能

原始碼上分析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。