集合操做(一)ArrayList,LinkedList源碼分析

ArrayList:

構造函數:

ArrayList提供了三種方式的構造器,能夠構造一個默認初始容量爲10的空列表、構造一個指定初始容量的空列表以及構造一個包含指定collection的元素的列表,這些元素按照該collection的迭代器返回它們的順序排列的。java

ArrayList底層是使用一個Object類型的數組來存放數據的。數組

transient Object[] elementData; // non-private to simplify nested class access

size變量表明List實際存放元素的數量安全

private int size;

不指定ArrayList大小時,默認數組大小爲10數據結構

private static final int DEFAULT_CAPACITY = 10;

接下來看幾個經常使用的方法:jvm

add:

1.函數

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 擴容檢測
    elementData[size++] = e; //新增元素加到末尾
    return true;
}

2.源碼分析

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  //擴容檢測
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index); //使用System.arraycopy的方法,將index後面元素日後移動1位
    elementData[index] = element;  // 存放元素到index位置
    size++;
}

set,get

get和set方法,都是經過數組下標,直接操做數據的,時間複雜度爲O(1)性能

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}
public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

咱們重點關注ArrayList的擴容策略,這也是咱們在實際工做中決定是否選擇ArrayList須要考慮的this

像以前add方法裏,每次增長元素時都會進行擴容檢測,若是數組大小不足,則會自動擴容;若是擴容後的大小超出數組最大的大小,則會拋出異常。spa

ensureCapacityInternal(size + 1); // 擴容檢測

那麼ArrayList最大長度能夠看一下定義,是Integer的最大長度-8,Integer是4個字節,那麼ArrayList的最大長度就是2^32-8

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

擴容:

接下來看一下具體的擴容過程:

ArrayList擴容方案,主要有兩個步驟:1.大小檢測,2.擴容

大小檢測

檢測數組大小是否爲0,若是是,則使用默認的擴容大小10

檢測是否須要擴容,只有當數組最小須要容量大小大於當前數組大小時,纔會進行擴容

擴容:grow和hugeCapacity

進行數組越界判斷

拷貝原始數據到新的數組中

private void ensureCapacityInternal(int minCapacity) {
    // 經過ArrayList<Integer> a = new ArrayList<Integer>()或者經過序列化讀取,元素大小爲0時,底層數組纔會爲null數組
    // 若是底層數組大小爲0,則使用默認的容量大小10
    if (elementData == EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    // 數據結構發生改變,和fail-fast機制有關,在使用迭代器過程當中,只能經過迭代器的方法(好比迭代器中add,remove等),修改List的數據結構,
    // 若是使用List的方法(好比List中的add,remove等),修改List的數據結構,會拋出ConcurrentModificationException
    modCount++;  


    // 當前數組容量大小不足時,纔會調用grow方法,自動擴容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

能夠看到擴容長度是經過位運算完成的

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 新的容量大小 = 原容量大小的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0) //溢出判斷,好比minCapacity = Integer.MAX_VALUE / 2, oldCapacity = minCapacity - 1
        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);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

fail-fast機制的實現

能夠看到在ArrayList中,有個modCount的變量,每次進行add,set,remove等操做,都會執行modCount++。

在獲取ArrayList的迭代器時,會將ArrayList中的modCount保存在迭代中,

每次執行add,set,remove等操做,都會執行一次檢查,調用checkForComodification方法,對modCount進行比較。若是迭代器中的modCount和List中的modCount不一樣,則拋出ConcurrentModificationException

final void checkForComodification() {
    if (expectedModCount != ArrayList.this.modCount)
        throw new ConcurrentModificationException();
}

在對集合進行迭代過程當中,除了迭代器能夠對集合進行數據結構上進行修改,其餘的對集合的數據結構進行修改,都會拋出ConcurrentModificationException錯誤。

序列化

transient Object[] elementData;

transient修飾符讓elementData沒法自動序列化,這樣的緣由是,數組內存儲的的元素其實只是一個引用,單單序列化一個引用沒有任何意義,反序列化後這些引用都沒法在指向原來的對象。ArrayList使用writeObject()實現手工序列化數組內的元素。

經過以上源碼分析,其實就能夠總結出ArrayList的優缺點以及使用場景

優勢:

1.get,set,時間複雜度爲O(1),查找元素快速

2.數據是順序存儲的

缺點:

1.add(通常都是在末尾插入),時間複雜度爲O(1),最差狀況下(往頭部插入數據),時間複雜度O(n)

2.remove,時間複雜度爲O(n),最優狀況下(移除末尾元素),時間複雜度爲O(1)

3.ArrayList底層使用數組存儲數據,數組是不能自動擴容的,所以在發生擴容的狀況下,須要移動大量的元素。

4.數組大小是由限制的,受jvm和機器的影響,當擴容超出上限時,ArrayList會拋出異常

5.ArrayList全部的操做,都不是同步的,所以ArrayList不是線程安全的。

使用場景:

插入操做多,數據量不大,順序存儲時,能夠考慮使用ArrayList

LinkedList

構造函數:

LinkedList有兩個構造參數,一個爲無參構造,只是新建一個空對象,第二個爲有參構造,新建一個空對象,而後把全部元素添加進去。

public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

LinkedList的存儲單元爲一個名爲Node的內部類,包含pre指針,next指針,和item元素,實現爲雙向鏈表

//鏈表的節點個數
transient int size = 0;

//指向頭節點的指針
transient Node<E> first;

//指向尾節點的指針
transient Node<E> last;

節點結構

Node 是在 LinkedList 裏定義的一個靜態內部類,它表示鏈表每一個節點的結構,包括一個數據域item,一個後置指針next,一個前置指針prev。

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;
    }
}

 

總結:

ArrayList和LinkedList在性能上各有優缺點,都有各自所適用的地方:

1.ArrayList的空間浪費主要體如今在list列表的結尾預留必定的容量空間,而LinkedList的空間花費則體如今它的每個元素都須要消耗至關的空間。

2.對ArrayList和LinkedList而言,在列表末尾增長一個元素所花的開銷都是固定的。對ArrayList而言,主要是在內部數組中增長一項,指向所添加的元素,偶爾可能會致使對數組從新進行分配;而對LinkedList而言,這個開銷是統一的,分配一個內部Entry對象。

使用場景:

當操做是在一列數據的後面添加數據而不是在前面或中間,而且須要隨機地訪問其中的元素時,使用ArrayList會提供比較好的性能;當你的操做是在一列數據的前面或中間添加或刪除數據,而且按照順序訪問其中的元素時,就應該使用LinkedList了。

相關文章
相關標籤/搜索