JAVA ArrayList的實現原理 及ArrayList的使用

一、 ArrayList概述:  

ArrayList是基於數組實現的,是一個動態數組,其容量能自動增長,類似於C語言中的動態申請內存,動態增長內存。

 

    ArrayList不是線程安全的,只能用在單線程環境下,多線程環境下可以考慮用Collections.synchronizedList(List l)函數返回一個線程安全的ArrayList類,也可以使用concurrent併發包下的CopyOnWriteArrayList類。

 

    ArrayList實現了Serializable接口,因此它支持序列化,能夠通過序列化傳輸,實現了RandomAccess接口,支持快速隨機訪問,實際上就是通過下標序號進行快速訪問,實現了Cloneable接口,能被克隆。

   每個ArrayList實例都有一個容量,該容量是指用來存儲列表元素的數組的大小。它總是至少等於列表的大小。隨着向ArrayList中不斷添加元素,其容量也自動增長。自動增長會帶來數據向新數組的重新拷貝,因此,如果可預知數據量的多少,可在構造ArrayList時指定其容量。在添加大量元素前,應用程序也可以使用ensureCapacity操作來增加ArrayList實例的容量,這可以減少遞增式再分配的數量。 
   注意,此實現不是同步的。如果多個線程同時訪問一個ArrayList實例,而其中至少一個線程從結構上修改了列表,那麼它必須保持外部同步。

 

二、 ArrayList的實現:

   對於ArrayList而言,它實現List接口、底層使用數組保存所有元素。其操作基本上是對數組的操作。下面我們來分析ArrayList的源代碼:

   1) 私有屬性:

   ArrayList定義只定義類兩個私有屬性:



 很容易理解,elementData存儲ArrayList內的元素,size表示它包含的元素的數量。

有個關鍵字需要解釋:transient。  

  Java的serialization提供了一種持久化對象實例的機制。當持久化對象時,可能有一個特殊的對象數據成員,我們不想用serialization機制來保存它。爲了在一個特定對象的一個域上關閉serialization,可以在這個域前加上關鍵字transient。

有點抽象,看個例子應該能明白。



被標記爲transient的屬性在對象被序列化的時候不會被保存。

接着回到ArrayList的分析中......

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

 

3) 元素存儲:

ArrayList提供了set(int index, E element)、add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)這些添加元素的方法。下面我們一一講解:


 

書上都說ArrayList是基於數組實現的,屬性中也看到了數組,具體是怎麼實現的呢?比如就這個添加元素的方法,如果數組大,則在將某個位置的值設置爲指定元素即可,如果數組容量不夠了呢?

    看到add(E e)中先調用了ensureCapacity(size+1)方法,之後將元素的索引賦給elementData[size],而後size自增。例如初次添加時,size爲0,add將elementData[0]賦值爲e,然後size設置爲1(類似執行以下兩條語句elementData[0]=e;size=1)。將元素的索引賦給elementData[size]不是會出現數組越界的情況嗎?這裏關鍵就在ensureCapacity(size+1)中了。

 

 4) 元素讀取:



5) 元素刪除:

 

ArrayList提供了根據下標或者指定對象兩種方式的刪除功能。如下:

 romove(int index):



首先是檢查範圍,修改modCount,保留將要被移除的元素,將移除位置之後的元素向前挪動一個位置,將list末尾元素置空(null),返回被移除的元素。

remove(Object o)


首先通過代碼可以看到,當移除成功後返回true,否則返回false。remove(Object o)中通過遍歷element尋找是否存在傳入對象,一旦找到就調用fastRemove移除對象。爲什麼找到了元素就知道了index,不通過remove(index)來移除元素呢?因爲fastRemove跳過了判斷邊界的處理,因爲找到元素就相當於確定了index不會超過邊界,而且fastRemove並不返回被移除的元素。下面是fastRemove的代碼,基本和remove(index)一致。



removeRange(int fromIndex,int toIndex)



執行過程是將elementData從toIndex位置開始的元素向前移動到fromIndex,然後將toIndex位置之後的元素全部置空順便修改size。

    這個方法是protected,及受保護的方法,爲什麼這個方法被定義爲protected呢?

    這是一個解釋,但是可能不容易看明白。http://stackoverflow.com/questions/2289183/why-is-javas-abstractlists-removerange-method-protected

    先看下面這個例子

 


 

輸出結果是[0, 1, 4, 5, 6],結果是不是像調用了removeRange(int fromIndex,int toIndex)!哈哈哈,就是這樣的。但是爲什麼效果相同呢?是不是調用了removeRange(int fromIndex,int toIndex)呢?

 

6) 調整數組容量ensureCapacity: 

   從上面介紹的向ArrayList中存儲元素的代碼中,我們看到,每當向數組中添加元素時,都要去檢查添加後元素的個數是否會超出當前數組的長度,如果超出,數組將會進行擴容,以滿足添加數據的需求。數組擴容通過一個公開的方法ensureCapacity(int minCapacity)來實現。在實際添加大量元素前,我也可以使用ensureCapacity來手動增加ArrayList實例的容量,以減少遞增式再分配的數量。

 

 從上述代碼中可以看出,數組進行擴容時,會將老數組中的元素重新拷貝一份到新的數組中,每次數組容量的增長大約是其原容量的1.5倍。這種操作的代價是很高的,因此在實際使用時,我們應該儘量避免數組容量的擴張。當我們可預知要保存的元素的多少時,要在構造ArrayList實例時,就指定其容量,以避免數組擴容的發生。或者根據實際需求,通過調用ensureCapacity方法來手動增加ArrayList實例的容量。

Object oldData[] = elementData;//爲什麼要用到oldData[]
乍一看來後面並沒有用到關於oldData, 這句話顯得多此一舉!但是這是一個牽涉到內存管理的類, 所以要了解內部的問題。 而且爲什麼這一句還在if的內部,這跟elementData = Arrays.copyOf(elementData, newCapacity); 這句是有關係的,下面這句Arrays.copyOf的實現時新創建了newCapacity大小的內存,然後把老的elementData放入。好像也沒有用到oldData,有什麼問題呢。問題就在於舊的內存的引用是elementData, elementData指向了新的內存塊,如果有一個局部變量oldData變量引用舊的內存塊的話,在copy的過程中就會比較安全,因爲這樣證明這塊老的內存依然有引用,分配內存的時候就不會被侵佔掉,然後copy完成後這個局部變量的生命期也過去了,然後釋放纔是安全的。不然在copy的的時候萬一新的內存或其他線程的分配內存侵佔了這塊老的內存,而copy還沒有結束,這將是個嚴重的事情。

 

  關於ArrayList和Vector區別如下:

  • ArrayList在內存不夠時默認是擴展50% + 1個,Vector是默認擴展1倍。
  • Vector提供indexOf(obj, start)接口,ArrayList沒有。
  • Vector屬於線程安全級別的,但是大多數情況下不使用Vector,因爲線程安全需要更大的系統開銷。

 

 ArrayList還給我們提供了將底層數組的容量調整爲當前列表保存的實際元素的大小的功能。它可以通過trimToSize方法來實現。代碼如下:

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

 

7)轉爲靜態數組toArray

 4、注意ArrayList的兩個轉化爲靜態數組的toArray方法。

    第一個, 調用Arrays.copyOf將返回一個數組,數組內容是size個elementData的元素,即拷貝elementData從0至size-1位置的元素到新數組並返回。

 


 

    第二個,如果傳入數組的長度小於size,返回一個新的數組,大小爲size,類型與傳入數組相同。所傳入數組長度與size相等,則將elementData複製到傳入數組中並返回傳入的數組。若傳入數組長度大於size,除了複製elementData外,還將把返回數組的第size個元素置爲空。

 

 

Fail-Fast機制: 
ArrayList也採用了快速失敗的機制,通過記錄modCount參數來實現。在面對併發的修改時,迭代器很快就會完全失敗,而不是冒着在將來某個不確定時間發生任意不確定行爲的風險。具體介紹請參考這篇文章深入Java集合學習系列:HashMap的實現原理 中的Fail-Fast機制。

 

總結:

關於ArrayList的源碼,給出幾點比較重要的總結:

    1、注意其三個不同的構造方法。無參構造方法構造的ArrayList的容量默認爲10,帶有Collection參數的構造方法,將Collection轉化爲數組賦給ArrayList的實現數組elementData。

    2、注意擴充容量的方法ensureCapacity。ArrayList在每次增加元素(可能是1個,也可能是一組)時,都要調用該方法來確保足夠的容量。當容量不足以容納當前的元素個數時,就設置新的容量爲舊的容量的1.5倍加1,如果設置後的新容量還不夠,則直接新容量設置爲傳入的參數(也就是所需的容量),而後用Arrays.copyof()方法將元素拷貝到新的數組(詳見下面的第3點)。從中可以看出,當容量不夠時,每次增加元素,都要將原來的元素拷貝到一個新的數組中,非常之耗時,也因此建議在事先能確定元素數量的情況下,才使用ArrayList,否則建議使用LinkedList。

    3、ArrayList的實現中大量地調用了Arrays.copyof()和System.arraycopy()方法。我們有必要對這兩個方法的實現做下深入的瞭解。

    首先來看Arrays.copyof()方法。它有很多個重載的方法,但實現思路都是一樣的,我們來看泛型版本的源碼:



很明顯調用了另一個copyof方法,該方法有三個參數,最後一個參數指明要轉換的數據的類型,其源碼如下:

按 Ctrl+C 複製代碼
按 Ctrl+C 複製代碼

這裏可以很明顯地看出,該方法實際上是在其內部又創建了一個長度爲newlength的數組,調用System.arraycopy()方法,將原來數組中的元素複製到了新的數組中。

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

 

4、ArrayList基於數組實現,可以通過下標索引直接查找到指定位置的元素,因此查找效率高,但每次插入或刪除元素,就要大量地移動元素,插入刪除元素的效率低。

5、在查找給定元素索引值等的方法中,源碼都將該元素的值分爲null和不爲null兩種情況處理,ArrayList中允許元素爲null。
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章