Java -- 基於JDK1.8的ArrayList原始碼分析
1,前言
很久沒有寫部落格了,很想念大家,18年都快過完了,才開始寫第一篇,爭取後面每週寫點,權當是記錄,因為最近在看JDK的Collection,而且ArrayList原始碼這一塊也經常被面試官問道,所以今天也就和大家一起來總結一下
2,原始碼解讀
當我們一般提到ArrayList的話都會脫口而出它的幾個特點:有序、可重複、查詢速度快,但是插入和刪除比較慢,執行緒不安全,那麼現在阿呆哥哥就會有這些疑問:為什麼說是有序的?怎麼有序?為什麼又說插入和刪除比較慢?為什麼慢?還有執行緒為什麼不安全?所以帶著這些問題,我們一一的來原始碼中來找找答案。
一般對於一個陌生的類,我們想使用它,都會先看它構造方法,再看它的屬性和方法,那麼我們也按照這種方式來讀讀ArrayList這個類
2.1構造方法
1 2 |
|
一般來說我們常見使用ArrayList的建立方式是上面的這兩種
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
final int DEFAULT_CAPACITY = 10 ;
|
上面是我們兩個構造方法和我們類中基本的屬性,從上面的程式碼上來看,在建立構造基本上都沒有做,且定義了兩個預設的空陣列,預設容器的大小DEFAULT_CAPACITY為10,還有我們真正儲存元素的地方elementData陣列,所以這就是為什麼說ArrayList儲存集合元素的底層時是使用陣列來實現,OK,上面的程式碼除了一個transient 修飾符之外我們同學們可能有點陌生之外,其餘的應該都能看的懂,transient 有什麼作用還有為什麼用它修飾elementData欄位,這個需要看完整個原始碼之後,我再來給大家解釋的話比較合適,這裡只需要留心一下。
還有一個不常用的構造方法
1 2 3 4 5 6 7 8 9 10 11 |
|
第2行:利用Collection.toArray()方法得到一個物件陣列,並賦值給elementData
第3行:size代表集合的大小,當通過別的集合來構造ArrayList的時候,需要賦值size
第5-6行:判斷 c.toArray()是否出錯返回的結果是否出錯,如果出錯了就利用Arrays.copyOf 來複制集合c中的元素到elementData陣列中
第9行:如果c中元素數量為空,則將EMPTY_ELEMENTDATA空陣列賦值給elementData
上面就是所有的建構函式的程式碼了,這裡我們可以看到,當建構函式走完之後,會創建出陣列elementData和初始化size,Collection.toArray()則是將Collection中所有元素賦值到一個數組,Arrays.copyOf()則是根據Class型別來決定是new還是反射來創造物件並放置到新的陣列中,原始碼如下:
1 2 3 4 5 6 7 8 9 |
|
這裡面System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length) 這個方法在我們的後面會的程式碼中會出現,就先講了,定義是:將陣列src從下標為srcPos開始拷貝,一直拷貝length個元素到dest陣列中,在dest陣列中從destPos開始加入先的srcPos陣列元素。相當於將src集合中的[srcPos,srcPos+length]這些元素新增到集合dest中去,起始位置為destPos
2.2 增加元素方法
一般經常使用的是下面三種方法
1 2 3 |
|
讓我們一個個來看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
第2行:呼叫ensureCapacityInternal()函式
第8-9行:判斷當前是否是使用預設的建構函式初始化,如果是設定最小的容量為預設容量10,即預設的elementData的大小為10(這裡是有一個容器的概念,當前容器的大小一般是大於當前ArrayList的元素個數大小的)
第16行:modCount欄位是用來記錄修改過擴容的次數,呼叫ensureExplicitCapacity()方法意味著確定修改容器的大小,即確認擴容
第26-30、35-44行:一般預設是擴容1.5倍,當時當發現還是不能滿足的話,則使用size+1之後的元素個數,如果發現擴容之後的值大於我們規定的最大值,則判斷size+1的值是否大於MAX_ARRAY_SIZE的值,大於則取值MAX_VALUE,反之則MAX_ARRAY_SIZE,也就數說容器最大的數量為MAX_VALUE
第32行:就是拷貝之前的資料,擴大陣列,且構建出一個新的陣列
第3行:這時候陣列擴容完畢,就是要將需要新增的元素加入到陣列中了
1 2 3 4 5 6 7 8 9 10 |
|
第2-3行:判斷插入的下標是否越界
第5行:和上面的一樣,判斷是否擴容
第6行:System.arraycopy這個方法的api在上面已經講過了,這裡的話則是將陣列elementData從index開始的資料向後移動一位
第8-9行:則是賦值index位置的資料,陣列大小加一
1 2 3 4 5 6 7 8 |
|
第2行:將集合轉成陣列,這時候原始碼沒有對c空很奇怪,如果傳入的Collection為空就直接空指標了
第3-7行:獲取陣列a的長度,進行擴容判斷,再將新傳入的陣列複製到elementData陣列中去
所以對增加資料的話主要呼叫add、addAll方法,判斷是否下標越界,是否需要擴容,擴容的原理是每次擴容1.5倍,如果不夠的話就是用size+1為容器值,容器擴充後modCount的值對應修改一次
2.3 刪除元素方法
常用刪除方法有以下三種,我們一個個來看看
1 2 3 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
從上面原始碼可以看出,如果要移除的元素為null和不為空,都是通過for迴圈找到要被移除元素的第一個下標,所以這裡我們就會思考,當我們的集合中有多個null的話,是不是呼叫remove(null)這個方法只會移除第一個出現的null元素呢?這個需要同學們下去驗證一下。然後通過System.arraycopy函式,來重新組合elementData中的值,且elementData[size]置空原尾部資料 不再強引用, 可以GC掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
可以看到remove(int index)更簡單了,都不需要通過for迴圈將要刪除的元素下邊確認下來,整體的邏輯和上面通過元素刪除的沒什麼區別,再來看看批量刪除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
第2、6-10行:對傳入集合c進行判空處理
第13-15行:定義區域性變數elementData、r、w、modified elementData用來重新指向成員變數elementData,用來儲存最終過濾後的元素,w用來紀錄過濾之後集合中元素的個數,modified用來返回這次是否有修改集合中的元素
第17-19行:for迴圈遍歷原有的elementData陣列,發現如果不是要移除的元素,則重新儲存在elementData,且w自增
第23-28行:如果出現異常,則會導致 r !=size , 則將出現異常處後面的資料全部複製覆蓋到數組裡。
第29-36行:判斷如果w!=size,則表明原先elementData陣列中有元素被移除了,然後將陣列尾端size-w個元素置空,等待gc回收。再修改modCount的值,在修改當前陣列大小size的值
2.3 修改元素方法
1 |
|
常見的方法也就是上面這一種,我們來看看它的實現的原始碼
1 2 3 4 5 6 7 8 |
|
原始碼很簡單,首先去判斷是否越界,如果沒有越界則將index下表的元素重新賦值element新值,將老值oldValue返回回去
2.4 查詢元素方法
1 |
|
讓我們看看原始碼
1 2 3 4 5 6 |
|
原始碼也炒雞簡單,首先去判斷是否越界,如果沒有越界則將index下的元素從elementData陣列中取出返回
2.5 清空元素方法
1 |
|
常見清空也就這一個方法
1 2 3 4 5 6 7 8 9 |
|
原始碼也很簡單,for迴圈重置每一個elementData陣列為空,修改size的值,修改modCount值
2.6 判斷是否存在某個元素
1 2 |
|
常見的一般是contains方法,不過我這裡像把lastIndexOf方法一起講了,原始碼都差不多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
通過上面的原始碼,大家可以看到,不管是contains方法還是lastIndexOf方法,其實就是進行for迴圈,如果找到該元素則記錄下當前元素下標,如果沒找到則返回-1,很簡單
2.7 遍歷ArrayList中的物件(迭代器)
1 2 3 4 |
|
我們遍歷集合中的元素方法挺多的,這裡我們就不講for迴圈遍歷,我們來看看專屬於集合的iterator遍歷方法吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
第1-3行:在獲取集合的迭代器的時候,去new了一個Itr物件,而Itr實現了Iterator介面,我們主要重點關注Iterator介面的hasNext、next方法
第12-16行:定義變數,limit:用來記錄當前集合的大小值;cursor:遊標,預設為0,用來記錄下一個元素的下標;lastRet:上一次返回元素的下標
第18-20行:判斷當前遊標cursor的值是否超過當前集合大小zise,如果沒有則說明後面還有元素
第24-31行:在這裡面做了不少執行緒安全的判斷,在這裡如果我們非同步的操作了集合就會觸發這些異常,然後獲取到集合中儲存元素的elemenData陣列
第32-33行:遊標cursor+1,然後返回元素 ,並設定這次次返回的元素的下標賦值給lastRet
3,看原始碼之前問題的反思
ok,上面的話基本上把我們ArrayList常用的方法的原始碼給看完了。這時候,我們需要來對之前的問題來一一進行總結了
①有序、可重複是什麼概念?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
可重複是指加入的元素可以重複,有序是指的加入元素的順序和取出來的時候順序相同,一般這個特點是List相對於Set和Map來比較出來的,後面我們把Set、Map的原始碼看了之後會更加理解這兩個特點
② 為什麼說查詢查詢元素比較快,但新增和刪除元素比較慢呢?
我們從上面的原始碼得到,當增加元素的時候是有可能會觸發擴容機制的,而擴容機制會導致陣列複製;刪除和批量刪除會導致找出兩個集合的交集,以及陣列複製操作;而查詢直接呼叫return (E) elementData[index]; 所以說增、刪都相對低效 而查詢是很高效的操作。
③ 為什麼說ArrayList執行緒是不安全
從上面的程式碼我們都知道,現在add()方法為例
1 2 3 4 5 6 |
|
這裡我們主要看兩點,第一點add()方法前面沒有synchronized欄位、第二點 elementData[size++] = e;這段程式碼可以拆開為下面兩部分程式碼
1 2 |
|
也就是說整個add()方法可以拆為兩步,第一步在elementData[Size] 的位置存放此元素,第二步增大 Size 的值。我們都知道我們的CUP是切換程序執行的,在單執行緒中這樣是沒有問題的,但是一般在我們專案中很多情況是在多執行緒中使用ArrayList的,這時候比如有兩個執行緒,執行緒 A 先將元素存放在位置 0。但是此時 CPU 排程執行緒A暫停,執行緒 B 得到執行的機會。執行緒B也向此 ArrayList 新增元素,因為此時 Size 仍然等於 0 ,所以執行緒B也將元素存放在位置0。然後執行緒A和執行緒B都繼續執行,都增加 Size 的值。這樣就會得到元素實際上只有一個,存放在位置 0,而 Size 卻等於 2。這樣就造成了我們的執行緒不安全了。
大家可以寫一個執行緒搞兩個執行緒來試試,看看size是不是有問題,這裡就不帶大家一起寫了。
④ transient 關鍵字有什麼用?
唉,這個就有點意思了,這個是我們之前讀原始碼讀出來的遺留問題,那原始碼現在讀完了,是時候來解決這個問題了,我們來看看transient官方給的解釋是什麼
1 |
|
然後我們看一下ArrayList的原始碼中是實現了java.io.Serializable序列化了的,也就是transient Object[] elementData; 這行程式碼的意思是不希望elementData被序列化,那這時候我們就有一個疑問了,為什麼elementData不進行序列化?這時候我去網上找了一下答案,覺得這個解釋是最合理且易懂的
1 |
|
這時候我們是懂了為什麼不給elementData進行序列化了,那當我們要使用序列化物件的時候,elementData裡面的資料是不是不能使用了?這裡ArrayList的原始碼提供了下面方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
通過writeObject方法將資料非null資料寫入到物件流中,再使用readObject讀取資料
4,總結
上面我們寫了這麼一大篇,是時候該來總結總結一下了
①查詢高效、但增刪低效,增加元素如果導致擴容,則會修改modCount,刪出元素一定會修改。 改和查一定不會修改modCount。增加和刪除操作會導致元素複製,因此,