溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

ArrayList、LinkedList和Vector的源碼解析,帶你走近List的世界

發布時間:2020-07-19 17:38:45 來源:網絡 閱讀:346 作者:Java筆記丶 欄目:編程語言

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

ArrayList、LinkedList和Vector的源碼解析,帶你走近List的世界

本文將通過剖析List接口的三個實現類——ArrayList、LinkedListVector的源碼,帶你走近List的世界。

ArrayList

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

底層實現

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

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

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

/**?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();?}?**/

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

實現了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;//底層數組中實際元素個數,區別于capacity

可以看到,默認第一次插入元素時創建數組的大小為10。當向容器中添加元素時,如果容量不足,容器會自動增加50%的容量。增加容量的函數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泛型的內在實現是通過類型擦除和類型強制轉換來進行的,其實存儲的數據類型都是Raw Type,因此集合框架的底層數組都是Object數組,可以容納任何對象。

數組復制

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

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

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

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

其調用了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;??}

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

非線程安全

ArrayList的實現是不同步的,如果多個線程同時訪問ArrayList實例,并且至少有一個線程修改list的結構,那么它就必須在外部進行同步。如果沒有這些對象, 這個list應該用Collections.synchronizedList()方法進行包裝。 最好在list的創建時就完成包裝,防止意外地非同步地訪問list:

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

除了未實現同步之外,ArrayList大致相當于Vector。

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

常用API

ArrayList常用的size(),?isEmpty(),?get(),set()方法實現都比較簡單,讀者可自行翻閱源碼,它們均能在常數時間內完成,性能很高,這也是數組實現的優勢所在。add()方法的時間開銷跟插入位置有關(adding n elements requires O(n) time),addAll()方法的時間開銷跟添加元素的個數成正比。其余方法大都是線性時間。

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]時可能會出現數組越界,這里的關鍵就在于ensureCapacity(size+1)的調用,這個方法的作用是確保數組的容量,它的源碼如下:

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來檢查列表中的元素是否發生結構性變化(列表元素數量發生改變)了,如果modCount值改變,則代表列表中元素發生了結構性變化。

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

可以看出,直接在數組后面插入一個元素add(e)效率也很高,但是如果要按下標來插入元素,則需要調用System.arraycopy()方法來移動部分受影響的元素,這會導致性能低下,這也是使用數組實現的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方法也分在末尾插入和在指定位置插入,先將入參中的集合c轉換成數組,根據轉換后數組的程度和ArrayList的size拓展容量,之后調用System.arraycopy()方法復制元素到相應位置,調整size。根據返回的內容分析,只要集合c的大小不為空,即轉換后的數組長度不為0則返回true。

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

trimToSize()方法

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

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

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

在實際應用中,考慮這樣一種情形,當某個應用需要,一個ArrayList擴容到比如size=10000,之后經過一系列remove操作size=15,在后面的很長一段時間內這個ArrayList的size一直保持在<100以內,那么就造成了很大的空間浪費,這時候建議顯式調用一下trimToSize()方法,以優化一下內存空間。 或者在一個ArrayList中的容量已經固定,但是由于之前每次擴容都擴充50%,所以有一定的空間浪費,可以調用trimToSize()消除這些空間上的浪費。

LinkedList

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

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

底層實現

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

publicclassLinkedList<E>extendsAbstractSequentialList<E>implementsList<E>,Deque<E>,Cloneable,java.io.Serializable

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

可以看到,LinkedList除了實現了List接口外,還實現了Deque接口,Deque即“Double Ended Queue”,是可以在兩端插入和移動數據的線性數據結構,我們熟知的隊列皆可以通過實現Deque接口來實現。因此在LinkedList的實現中,除了提供了列表相關的方法如add()、remove()等,還提供了棧和隊列的pop()、peek()、poll()、offer()等相關方法。這些方法中有些彼此之間只是名稱的區別,內部實現完全相同,以使得這些名字在特定的上下文中顯得更加的合適。

LinkedList定義的字段如下:

transientintsize=0;transientNode<E>first;transientNode<E>last;

Size代表的是鏈表中存儲的元素個數,而first和last分別代表鏈表的頭節點尾節點。 其中Node是LinkedList定義的靜態內部類:

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

常用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在表尾添加元素,只要直接修改相關節點的前后繼節點信息,而無需移動其他元素的位置,因此執行添加操作時效率很高。此外,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方法的底層實現也無需移動列表里其他元素的位置,而只需要修改被刪除節點及其前后節點的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對應位置的元素,必須要遍歷整個列表,在源碼實現中已經使用了二分查找size >> 1即是除以2)的方法來進行優化,但查找元素的開銷依然很大,并且與查找的位置有關。相比較ArrayList的常數級時間的消耗而言,差距很大。

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

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

Vector

作為伴隨JDK早期誕生的容器,Vector現在基本已經被棄用,不過依然有一些老版本的代碼使用到它,因此也有必要做一些了解。VectorArrayList的實現基本相同,它們底層都是基于Object數組實現的,兩者最大的區別在于ArrayList是非線程安全的,而Vector是線程安全的。由于Vector與ArrayList的實現非常相近,前面對于ArrayList已經進行過詳細介紹了,這里很多東西就不在贅述,重點介紹Vector與ArrayList的不同之處。

容量擴展

Vector與ArrayList還有一處細節上的不同,那就是Vector進行添加操作時,如果列表容量不夠需要擴容,每次增加的大小是原來的100%,而前面已經講過,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類內部的大部分方法與ArrayList均相同,區別僅在于加上了synchronized關鍵字,比如:

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

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

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

Stack類

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

總結

  • ArrayList是最常用的List實現類,內部是通過數組實現的,它允許對元素進行快速隨機訪問。數組的缺點是每個元素之間不能有間隔,當數組大小不滿足時需要增加存儲能力,就要講已經有數組的數據復制到新的存儲空間中。當從ArrayList的中間位置插入或者刪除元素時,需要對數組進行復制、移動、代價比較高。因此,它適合隨機查找和遍歷,不適合插入和刪除。

  • LinkedList是用鏈表結構存儲數據的,很適合數據的動態插入和刪除,隨機訪問和遍歷速度比較慢。另外,他還提供了List接口中沒有定義的方法,專門用于操作表頭和表尾元素,可以當作堆棧、隊列和雙向隊列使用。

  • Vector與ArrayList一樣,也是通過數組實現的,不同的是它支持線程的同步,即某一時刻只有一個線程能夠寫Vector,避免多線程同時寫而引起的不一致性,但實現同步需要很高的花費,因此,訪問它比訪問ArrayList慢,現在基本已棄用。


向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女