Java源碼之ArrayList

總體介紹


Collection接口是集合類的根接口,Java中沒有提供這個接口的直接的實現類。Set和List兩個類繼承於它。Set中不能包含重複的元素,也沒有順序來存放。而List是一個有序的集合,可以包含重複的元素。

而Map又是另一個接口,它和Collection接口沒有關係。Map包含了key-value鍵值對,同一個Map裏key是不能重複的,而不同key的value是可以相同的。

在這裏借用一張別人總結的對比圖進行總結:

成員變量


首先我們來看看ArrayList的成員變量:


可以看到主要的幾個成員變量如上(跟進繼承的父類,父父類直到根父類都沒有成員變量)。我們來一一介紹一下。首先是一個常量DEFAULT_CAPACITY,根據註釋表示默認的長度爲10。然後是一個EMPTY_ELEMENTDATA的常量object數組,只是空有其表沒有內容。然後是一個object數組elementData。

這個就是最重要的成員了,通過註釋我們可以看到這表示這個數組用來存儲我們的數據。也就是說,我們代碼中的add的數據都會放在這個數組裏面。那麼由此我們可知,ArrayList內部是由數組實現。再看最後一個變量,int類型的size。

第一眼還以爲是elementData數組的長度。仔細看註釋,才發現它表示的是elementData數組裏麪包含的數據長度。

構造函數


介紹完了成員變量,我們來看看構造方法:


我們看到主要有三個構造方法。


(1)第一個構造方法需要傳入一個int類型的變量。表示我們實例化一個ArrayList的時候想讓ArrayList的初始化長度爲多少。然後如果該變量大於0,那麼new一個長度爲傳入值的對象數組。如果傳入爲0,那麼等於EMPTY_ELEMENTDATA。這個變量我們上面講過,就是實例化一個對象數組,內容爲空。如果小於0,那麼拋出異常。

(2)第二個構造方法是無參構造方法,直接等於DEFAULTCAPACITY_EMPTY_ELEMENTDATA。沒有其他的好說的。

(3)第三個則我們另外一種常見的使用方法,比如處理化AList的時候想把BList的值傳給AList。

那麼使用如下代碼:


我們看看構造函數做了什麼。我們看到首先調用了c.toArray()方法將我們傳入的集合元素變成一個數組來賦值給elementData數組。

然後判斷elementData數組裏面是否有數據元素,如果有,那麼再判斷elementData數組類型是否爲Object,不是的話,轉爲Object類型。如果沒有元素,那麼直接賦值爲EMPTY_ELEMENTDATA。

至此三個構造方法就已經分析完了,基本上沒有什麼難度。


常見方法



接下來我們來分析一些ArrayList的常見方法。


size()


很簡單,就是將elementData數組中元素個數返回。


isEmpty()


也很簡單,就是判斷sizes是否等於0,即elementData數組中是否有元素。


add()



可以看到add()方法還是比較簡短的,接下面我們一步一步的分析,第一行是調用了一個方法,第二行是常見的數組賦值,將下標爲size處的數組元素賦值爲e,然後size自加1。如果有意識的話,會想到,咦?這麼做的話,不怕數組越界??那麼我們去第一行代碼的方法裏看看:


首先第一個方法中先判斷elementData是否是沒有元素的數組(但並不是elemetData爲null)。如果是,那麼取我們傳入的值(也就是size + 1)和默認的數組長度(長度爲10)中的最大值。然後調用了ensureExplicitCapacity()方法。我們繼續看這個方法:



首先是一個int類型的成員變量modCount自加,這個變量是ArrayList的父類AbstractList的一個成員,用來表示List的修改次數。接下來有一個判斷,用傳入的值減去當前elementData的長度,如果大於0,調用grow()方法(我個人理解爲擴展的意思),這裏其實也能猜出大概意思。如果我們所需的最小數組長度已經比當前數組長度大了,那麼就需要我們擴展數組了。我們接着看grow()方法:



我們接着看代碼,首先保存當前數組長度到oldCapacity,然後定義一個newCapacity,新長度爲舊長度的3/2,也就是增加一半的容量。然後用新長度減去所需最小長度,如果小於0,意味着新長度比所需長度還要小,那麼就直接將新長度改爲所需最小長度。

然後新長度如果超過了允許的數組最大長度,調用hugeCapacity()方法進行調整。最後處理完畢後,調用Arrays.copyOf()方法賦值給elementData。至此就把elementData數組擴展完畢。然後回到add方法中直接賦值 elementData[size++] = e即可。

我們來看第二個add()方法:


上面的方法也是我們常用的,將指定的下標處元素賦值爲我們設定的值。最開始我用這個方法的時候一直很擔心假設我把指定位置設置了值,那原來的值會不會被覆蓋呢?

我們看一下實現代碼解惑一下,也很簡單。首先檢查index索引是否比elementData中擁有元素的數量大或者小於0。有問題則拋出異常。負責又調用ensureCapacityInternal()方法來確認數組長度是否足夠。然後調用System.arraycopy()方法,我們來看看:



可以看到是個native方法,我們就不跟源碼了,看看參數含義就明白了。src就是源數組,srcPos就是表明從源數組的下標多少開始複製,dest和destPos就是對應的目的數組,複製源數組的數據到從目的數組的下標開始存放,length就是打算複製多少個源數組的值。

說了半天有點繞口,看看上面的調用例子,我們用具體例子來講解。首先源數組是elementData,假設有6個元素(即size爲6,但是elementData的長度大於6),index假設爲3,length爲size - index爲3。而dest也爲elementData,destPos爲index + 1 等於4。

所以整體就是從index(3)下標處即elementData[3]處開始往後拿3個值,複製到elementDatadestPos開始往後3個值。

其實解釋了半天就是將我們要插入的位置開始的元素全部往後移了一個位置。然後把值插入到指定的位置。(我真聰明)所以之前的擔心就多餘啦。我們插入到指定位置,指定位置的舊值會往後移,並不會被覆蓋。

clear()



clear()方法也很簡單,首先modCount自加,表示我們對list進行了操作。然後for循環置空即可,最後設置size等於0。

remove()

remove()也有兩個方法,我們來看第一個:


根據傳入參數我們也能猜到意思,就是移除指定下標的元素。

首先還是檢查index是否有效。然後modCount++,表示我們對list又進行一次操作。然後將指定下標的元素取出。然後計算出我們需要移動多少個元素,指的是從刪除位置往後的元素,不包括刪除位置的元素。如果個數大於0,那麼調用 System.arraycopy()方法將刪除位置後的一個元素開始到最後的元素往前移動一個位置。

然後將size立馬自減,然後將最後一個位置置爲null(因爲元素往前移動一位,那麼最後一個元素往前移後,原來的最後一個位置值還存在沒有被覆蓋)。

最後返回舊的刪除位置的元素值


接下來我們來看第二個remove()方法:



很明顯看參數也猜得出是直接移除掉我們某個元素。首先判斷我們傳入的object是否爲空,如果爲空,那麼就for循環找到第一個數組中值爲null的元素,調用fastRemove()方法,我們去看看:



從註釋看出,其實這就是第一個remove()方法的簡化版,取消了越界檢查,並且設置返回類型爲void,不再返回刪除的舊值。這裏就不再分析。

接着上面說,如果remove()中如果傳入的對象不爲null,那麼就是for循環找到這個值然後移除即可。整個函數返回類型爲boolean,true表示有這個對象刪除成功。沒有表示數組裏沒有這個對象,沒有進行刪除操作。

contains()



contains()也是我們經常使用的方法,用來查詢當前ArrayList是否包含某個對象。我們看調用了一個indexOf()方法然後把返回值和0進行比較(乍一看還是很奇怪的,返回布爾值不好嗎),我們來看看indexOf()方法:
indexOf()


我們來看看代碼,首先是對傳入對象的判空。如果對象爲空,還是一樣的,for循環來查找elementData中第一個爲null的元素,然後返回下標。如果傳入對象不爲空,那麼一樣for循環查找第一個匹配元素,然後返回第一個匹配元素的下標。如果都找不到,那麼就返回-1。

看完這個方法,就明白了爲啥不用返回值布爾類型了,原來是返回下標來和0進行判斷是否包含。但是我們可以看到其實contains()方法並沒有返回元素下標。

所以本人第一次看完代碼覺得這是多此一舉。後來突然想到indexOf()方法是一個public方法,也是我們經常使用的方法。可能就在這裏java編寫者進行方法重用就不必再重複寫新方法來判斷。

順帶着我們就把indexOf()方法介紹,方法就是返回第一個匹配傳入對象的元素下標。如果數組中沒有匹配元素那麼返回-1。


get()


這個方法可以說是最最常用的方法了,但是其實我們看到非常簡單,就是進行一個下標的越界判斷,然後返回elementData[index]元素。

set()

set方法也是,其實set(int index, E element)和add(int index, E element)方法很相似。只是set是將指定位置的值直接覆蓋掉,而add()則是將指定位置開始的元素往後全部後移一位,舊值不會被覆蓋掉。set()方法沒有什麼可以多分析的代碼。

至此我們常見的ArrayList的方法源碼分析就已經完了,其他的一些方法要麼不怎麼用,要麼非常簡單隻有一兩行簡單代碼,讀者一跟進去就能明白。


總結


  • 首先ArrayList內部是由數組來實現的。而且在存放數據的數組長度不夠時,會進行擴容,即增加數組長度。在Java 8中是默認擴展爲原來的1.5倍。

  • 既然是數組,那麼優點就是查找某個元素很快。可以通過下標查找元素,查找效率高。但是由此也看出缺點,每次刪除元素,都會進行大量的數組元素移動,複製新的數組等等。增加元素的話如果長度不夠,還要進行擴容。因此刪除效率低。如果我們在實際開發中能夠清楚知道我們的數據量,建議創建ArrayList的時候指定長度,這樣無需頻繁增加數據時不斷進行擴容



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