Java集合框架源碼解讀(1)——ArrayList、LinkedList和Vector

Java集合框架源碼解讀(1)——ArrayList、LinkedList和Vector

java.util.List接口是Java Collections Framework的一個重要組成部分,List接口的架構(gòu)圖如下:

img

本文將通過剖析List接口的三個實現(xiàn)類——ArrayListLinkedListVector的源碼,帶你走近List的世界。

ArrayList

ArrayList是List接口可調(diào)整數(shù)組大小的實現(xiàn)。實現(xiàn)所有可選列表操作,并允許放入包括空值在內(nèi)的所有元素。每個ArrayList都有一個容量(capacity,區(qū)別于size),表示底層數(shù)組的實際大小,容器內(nèi)存儲元素的個數(shù)不能多于當前容量。

底層實現(xiàn)

java.util.ArrayList類的繼承關系如下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

其中需要注意的是RandomAccess接口,這是一個標記接口,沒有定義任何具體的內(nèi)容,該接口的意義是隨機存取數(shù)據(jù)。在該接口的注釋中有這樣一段話:

/** for (int i=0, n=list.size(); i < n; i++) { list.get(i); } runs faster than this loop: for (Iterator i=list.iterator(); i.hasNext(); ) { i.next(); } **/

這說明在數(shù)據(jù)量很大的情況下,采用迭代器遍歷實現(xiàn)了該接口的集合,速度比較慢。

實現(xiàn)了RandomAccess接口的集合有:ArrayList, AttributeList, CopyOnWriteArrayList, RoleList, RoleUnresolvedList, Stack, Vector等。

ArrayList一些重要的字段如下:

    private static final int DEFAULT_CAPACITY = 10;
    private static final Object[] EMPTY_ELEMENTDATA = {};
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    transient Object[] elementData; // non-private to simplify nested class access
    private int size;//底層數(shù)組中實際元素個數(shù),區(qū)別于capacity

可以看到,默認第一次插入元素時創(chuàng)建數(shù)組的大小為10。當向容器中添加元素時,如果容量不足,容器會自動增加50%的容量。增加容量的函數(shù)grow()源碼如下:

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//右移一位代表增加50%
        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);
    }

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

值得注意的是,由于集合框架用到了編譯器提供的語法糖——泛型,而Java泛型的內(nèi)在實現(xiàn)是通過類型擦除和類型強制轉(zhuǎn)換來進行的,其實存儲的數(shù)據(jù)類型都是Raw Type,因此集合框架的底層數(shù)組都是Object數(shù)組,可以容納任何對象。

數(shù)組復制

ArrayList的實現(xiàn)中大量地調(diào)用了Arrays.copyof()System.arraycopy()方法。在此介紹一下這兩個方法。

System.arraycopy()方法是一個native方法,調(diào)用了系統(tǒng)的C/C++代碼,在openJDK中可以看到其源碼。該方法最終調(diào)用了C語言的memmove()函數(shù),因此它可以保證同一個數(shù)組內(nèi)元素的正確復制和移動,比一般的復制方法的實現(xiàn)效率要高很多,很適合用來批量處理數(shù)組。Java強烈推薦在復制大量數(shù)組元素時使用該方法,以取得更高的效率。

Arrays.copyOf()方法有很多重載版本,但實現(xiàn)思路都是一樣的,其泛型版本源碼如下:

public static <T> T[] copyOf(T[] original, int newLength) {     return (T[]) copyOf(original, newLength,    original.getClass());  
}  

其調(diào)用了copyof()方法:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {  
    T[] copy = ((Object)newType == (Object)Object[].class)  
        ? (T[]) new Object[newLength]  
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);  
    System.arraycopy(original, 0, copy, 0,  
                     Math.min(original.length, newLength));  
    return copy;  
}    

該方法實際上是在其內(nèi)部創(chuàng)建了一個類型為newType、長度為newLength的新數(shù)組,調(diào)用System.arraycopy()方法,將原來數(shù)組中的元素復制到新的數(shù)組中。

非線程安全

ArrayList的實現(xiàn)是不同步的,如果多個線程同時訪問ArrayList實例,并且至少有一個線程修改list的結(jié)構(gòu),那么它就必須在外部進行同步。

如果沒有這些對象, 這個list應該用Collections.synchronizedList()方法進行包裝。 最好在list的創(chuàng)建時就完成包裝,防止意外地非同步地訪問list:

List list = Collections.synchronizedList(new ArrayList(...));

除了未實現(xiàn)同步之外,ArrayList大致相當于Vector。

size(),isEmpty(),get(),set()方法均能在常數(shù)時間內(nèi)完成,add()方法的時間開銷跟插入位置有關(adding n elements requires O(n) time),addAll()方法的時間開銷跟添加元素的個數(shù)成正比。其余方法大都是線性時間。

常用API

ArrayList常用的size(),isEmpty(),get(),set()方法實現(xiàn)都比較簡單,讀者可自行翻閱源碼,它們均能在常數(shù)時間內(nèi)完成,性能很高,這也是數(shù)組實現(xiàn)的優(yōu)勢所在。

add()方法的時間開銷跟插入位置有關(adding n elements requires O(n) time),addAll()方法的時間開銷跟添加元素的個數(shù)成正比。其余方法大都是線性時間。

add()方法

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
}
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尾部插入一個元素,后者是在指定位置插入元素。值得注意的是,將元素的索引賦給elementData[size]時可能會出現(xiàn)數(shù)組越界,這里的關鍵就在于ensureCapacity(size+1)的調(diào)用,這個方法的作用是確保數(shù)組的容量,它的源碼如下:

ensureCapacity()ensureExplicitCapacity()方法:

public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
}

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
}

其中有一個重要的實例變量modCount,它是在AbstractList類中定義的,在使用迭代器遍歷的時候,用modCount來檢查列表中的元素是否發(fā)生結(jié)構(gòu)性變化(列表元素數(shù)量發(fā)生改變)了,如果modCount值改變,則代表列表中元素發(fā)生了結(jié)構(gòu)性變化。

前面說過,ArrayList是非線程安全的,modCount主要在多線程環(huán)境下進行安全檢查,防止一個線程正在迭代遍歷,另一個線程修改了這個列表的結(jié)構(gòu)。如果在使用迭代器進行遍歷ArrayList的時候modCount值改變,則會報ConcurrentModificationException異常。

可以看出,直接在數(shù)組后面插入一個元素add(e)效率也很高,但是如果要按下標來插入元素,則需要調(diào)用System.arraycopy()方法來移動部分受影響的元素,這會導致性能低下,這也是使用數(shù)組實現(xiàn)的ArrayList的劣勢。

同理,remove()方法也會改變modCount的值,效率與按下標插入元素相似,在此不加贅述。

addAll()方法

public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
}
public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
}

addAll方法也分在末尾插入和在指定位置插入,先將入?yún)⒅械募蟘轉(zhuǎn)換成數(shù)組,根據(jù)轉(zhuǎn)換后數(shù)組的程度和ArrayList的size拓展容量,之后調(diào)用System.arraycopy()方法復制元素到相應位置,調(diào)整size。根據(jù)返回的內(nèi)容分析,只要集合c的大小不為空,即轉(zhuǎn)換后的數(shù)組長度不為0則返回true。

容易看出,addAll()方法的時間開銷是跟添加元素的個數(shù)成正比的。

trimToSize()方法

下面來看一個簡單但是很有用的方法trimToSize()。

public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
}

由于elementData的長度會被拓展,size標記的是其中包含的元素的個數(shù)。所以會出現(xiàn)size很小但elementData.length很大的情況,將出現(xiàn)空間的浪費。trimToSize()將返回一個新的數(shù)組給elementData,元素內(nèi)容保持不變,length和size相同,節(jié)省空間。

在實際應用中,考慮這樣一種情形,當某個應用需要,一個ArrayList擴容到比如size=10000,之后經(jīng)過一系列remove操作size=15,在后面的很長一段時間內(nèi)這個ArrayList的size一直保持在<100以內(nèi),那么就造成了很大的空間浪費,這時候建議顯式調(diào)用一下trimToSize()方法,以優(yōu)化一下內(nèi)存空間。

或者在一個ArrayList中的容量已經(jīng)固定,但是由于之前每次擴容都擴充50%,所以有一定的空間浪費,可以調(diào)用trimToSize()消除這些空間上的浪費。

LinkedList

LinkedList與ArrayList一樣也實現(xiàn)了List接口,LinkedList使用雙向鏈表實現(xiàn),允許存儲元素重復,鏈表與ArrayList的數(shù)組實現(xiàn)相比,在進行插入和刪除操作時效率更高,但查找操作效率更低,因此在實際使用中應根據(jù)自己的程序計算需求來從二者中取舍。

與ArrayList一樣,LinkedList也是非線程安全的。

底層實現(xiàn)

java.util.LinkedList的繼承關系如下:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList繼承自抽象類AbstractSequenceList,其實AbstractSequenceList已經(jīng)實現(xiàn)了List接口,這里標注出List只是更加清晰而已。AbstractSequenceList提供了List接口骨干性的實現(xiàn)以減少從而減少了實現(xiàn)受“連續(xù)訪問”數(shù)據(jù)存儲(如鏈表)支持的此接口所需的工作。對于隨機訪問數(shù)據(jù)(如數(shù)組),則應該優(yōu)先使用抽象類AbstractList。

可以看到,LinkedList除了實現(xiàn)了List接口外,還實現(xiàn)了Deque接口,Deque即“Double Ended Queue”,是可以在兩端插入和移動數(shù)據(jù)的線性數(shù)據(jù)結(jié)構(gòu),我們熟知的隊列皆可以通過實現(xiàn)Deque接口來實現(xiàn)。

因此在LinkedList的實現(xiàn)中,除了提供了列表相關的方法如add()、remove()等,還提供了棧和隊列的pop()、peek()、poll()offer()等相關方法。這些方法中有些彼此之間只是名稱的區(qū)別,內(nèi)部實現(xiàn)完全相同,以使得這些名字在特定的上下文中顯得更加的合適。

LinkedList定義的字段如下:

transient int size = 0;
transient Node<E> first;
transient Node<E> last;

Size代表的是鏈表中存儲的元素個數(shù),而first和last分別代表鏈表的頭節(jié)點尾節(jié)點。 其中Node是LinkedList定義的靜態(tài)內(nèi)部類:

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是鏈表的節(jié)點類,其中的三個屬性item、next、prev分別代表了節(jié)點的存儲屬性值、前繼節(jié)點和后繼節(jié)點。

常用API

add(e)方法

public boolean add(E e) {
        linkLast(e);
        return true;
}

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
        modCount++;
}

由上述代碼可見,LinkedList在表尾添加元素,只要直接修改相關節(jié)點的前后繼節(jié)點信息,而無需移動其他元素的位置,因此執(zhí)行添加操作時效率很高。此外,LinkedList也是非線程安全的

remove(o)方法

public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

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

add方法一樣,remove方法的底層實現(xiàn)也無需移動列表里其他元素的位置,而只需要修改被刪除節(jié)點及其前后節(jié)點的prevnext屬性即可。

get(index)方法

該方法可以返回指定位置的元素,下面來看一看代碼

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要想找到index對應位置的元素,必須要遍歷整個列表,在源碼實現(xiàn)中已經(jīng)使用了二分查找size >> 1即是除以2)的方法來進行優(yōu)化,但查找元素的開銷依然很大,并且與查找的位置有關。相比較ArrayList的常數(shù)級時間的消耗而言,差距很大。

clear()方法

public void clear() {
    // Clearing all of the links between nodes is "unnecessary", but:
    // - helps a generational GC if the discarded nodes inhabit
    // more than one generation
    // - is sure to free memory even if there is a reachable Iterator
    for (Node<E> x = first; x != null; ) {
        Node<E> next = x.next;
        x.item = null;
        x.next = null;
        x.prev = null;
        x = next;
    }
    first = last = null;
    size = 0;
    modCount++;
}

該方法并不復雜,作用只是遍歷列表,清空表中的元素和節(jié)點連接而已。之所以單獨拿出來講,是基于GC方面的考慮,源碼注釋中講道,該方法中將所有節(jié)點之間的“連接”都斷開并不是必要的,但是由于鏈表中的不同節(jié)點可能位于分代GC的不同年代中,如果它們互相引用會給GC帶來一些額外的麻煩,因此執(zhí)行此方法斷開節(jié)點間的相互引用,可以幫助分代GC在這種情況下提高性能。

Vector

作為伴隨JDK早期誕生的容器,Vector現(xiàn)在基本已經(jīng)被棄用,不過依然有一些老版本的代碼使用到它,因此也有必要做一些了解。

VectorArrayList的實現(xiàn)基本相同,它們底層都是基于Object數(shù)組實現(xiàn)的,兩者最大的區(qū)別在于ArrayList是非線程安全的,而Vector是線程安全的。由于Vector與ArrayList的實現(xiàn)非常相近,前面對于ArrayList已經(jīng)進行過詳細介紹了,這里很多東西就不在贅述,重點介紹Vector與ArrayList的不同之處。

容量擴展

Vector與ArrayList還有一處細節(jié)上的不同,那就是Vector進行添加操作時,如果列表容量不夠需要擴容,每次增加的大小是原來的100%,而前面已經(jīng)講過,ArrayList一次只增加原有容量的50%。具體代碼如下:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
 capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

線程安全

Vector類內(nèi)部的大部分方法與ArrayList均相同,區(qū)別僅在于加上了synchronized關鍵字,比如:

public synchronized void trimToSize() {
    modCount++;
    int oldCapacity = elementData.length;
    if (elementCount < oldCapacity) {
        elementData = Arrays.copyOf(elementData, elementCount);
    }
}

這也保證了同一時刻只有一個線程能夠?qū)慥ector,避免多線程同時寫而引起的不一致性,但實現(xiàn)同步需要很高的花費,因此,訪問Vector比訪問ArrayList要慢。

前面說過,由于性能和一些設計問題,Vector現(xiàn)在基本已被棄用,當涉及到線程安全時,可以如前文介紹ArrayList時所說的,對ArrayList進行簡單包裝,即可實現(xiàn)同步。

Stack類

Vector還有一個子類叫Stack,其實現(xiàn)了棧的基本操作。這也是在JDK早期出現(xiàn)的容器,很多設計不夠規(guī)范,現(xiàn)在已經(jīng)過時,使用Queue接口的相關實現(xiàn)可以完全取代它。

總結(jié)

  • ArrayList是最常用的List實現(xiàn)類,內(nèi)部是通過數(shù)組實現(xiàn)的,它允許對元素進行快速隨機訪問。數(shù)組的缺點是每個元素之間不能有間隔,當數(shù)組大小不滿足時需要增加存儲能力,就要講已經(jīng)有數(shù)組的數(shù)據(jù)復制到新的存儲空間中。當從ArrayList的中間位置插入或者刪除元素時,需要對數(shù)組進行復制、移動、代價比較高。因此,它適合隨機查找和遍歷,不適合插入和刪除。
  • LinkedList是用鏈表結(jié)構(gòu)存儲數(shù)據(jù)的,很適合數(shù)據(jù)的動態(tài)插入和刪除,隨機訪問和遍歷速度比較慢。另外,他還提供了List接口中沒有定義的方法,專門用于操作表頭和表尾元素,可以當作堆棧、隊列和雙向隊列使用。
  • Vector與ArrayList一樣,也是通過數(shù)組實現(xiàn)的,不同的是它支持線程的同步,即某一時刻只有一個線程能夠?qū)慥ector,避免多線程同時寫而引起的不一致性,但實現(xiàn)同步需要很高的花費,因此,訪問它比訪問ArrayList慢,現(xiàn)在基本已棄用。
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容