JAVA開發有很多常用的數據類型,例如:ArrayList,Vector,HashMap,HashTable,StringBuffer,StringBuilder等。本文嘗試從以下三個方面分析ArrayList源碼。
問題:
【Question1】:ArrayList如何存儲數據?
【Question2】:都說ArrayList非線程安全,Vector是線程安全,究竟怎么回事?
【Question3】:從性能方面考慮,使用ArrayList需要注意什么?
【Answer1】:類名當中的array就已經說明,它是以數組的形式存儲數據的。那么,緊接著3個問題來了:
1.1 數組初始容量capacity為多大?如果太小,很快就裝滿了。如果過大,數組又沒被填滿,就會造成內存的浪費。
1.2 數組被填滿之后再往里加數據,數組怎么應對?
1.3 數據太多,多到超過Integer.MAX_VALUE。數組怎么應對?
answer1.1:兩個辦法,要么用戶自己指定capacity的大小;要么給capacity一個初始值。ArrayList的實現中:
辦法一:讓用戶自己指定初始容量。此辦法可以申請少于10個元素容量的數組。
辦法二:初始容量為0。第一次添加數據時,申請默認容量為10的數組空間。把數據放入數組。此策略優點是,實例化ArrayList又不用時,可以避免內存浪費。該策略稱為惰性初始化。缺點,無法申請少于10個元素容量的數組。
answer1.2:add數據過程:
先檢查數組是否還有剩余元素空間,再添加數據。如果有,直接數組末尾添加數據。如果沒有,就先把當前數組copy到一個長50%的空數組中,再往新數組的尾部添加元素。
[舉例]
ArrayList添加元素過程與用整理箱裝東西類似。在沒有東西要裝之前是沒有買整理箱的,在有了第一件需要裝起來的物品之后,買了一個只能裝10件物品的小整理箱,然后把第一件物品放進去。再有新物品以來時,先檢查整理箱是還有空間,如果有就往里面放;如果沒有就買一個比現在整理箱大50%的新整理箱,然后舊整理箱里的物品全部轉移動新整理箱中,然后把新物品放入新整理箱。如此持續下去,當發現需要裝的物品太多,多到整個房子都裝不下了,就拋異常。
answer1.3:直接拋異常。
【Answer2】:Vector在add,get方法前加了"synchronized",而ArrayList沒有。
【Answer3】:實例化ArrayList時,指定ArrayList的容量。
copy數組的過程很費時間。所以,為了提高性能,盡可能事先估計列表長度,在實例化ArrayList時就指定長度。以避免添加元素過程中,反復通過copy數組來擴容。
[實驗驗證]:
1、實驗過程:
往ArrayList添加300w個字符串(隨機生成的int),分別執行10次,經過初始化長度的ArrayList平均用時50.1ms;未初始化長度的ArrayList平均用時148.1ms,每次添加300w個字符串,ArrayList擴容33次,也就是copy數組33次。
2、實驗結論:
2.1、實例化ArrayList時指定ArrayList的容量,確實可以提高性能;
2.2、性能相差3倍;
3、結論分析:
3.1、性能相差3倍,但性能差別的絕對值只有100ms左右,相差不是很大。原因很可能是實驗用的字符串對象比較小,所以copy數組的速度很快。如果是大型的POJO對象,差別會更明顯。
3.2、300w條字符串add操作在200ms之內完成。所以,如果要添加的對象不多,對象也不大,可以不用太關心性能的問題。
3.3、如果是大對象或者對象又比較多,最好考慮指定初始容量大小。當然,也可以在大量添加對象之前,調用
ensureCapacity方法預先擴容。
【后續任務】
數組擴容的其他算法。在ArrayList中采用了數組copy的辦法來擴容,可以考慮別的擴容算法。