? ? ? ? 今天我們來聊聊Java集合中的ArrayList,說起集合在我們編碼過程中使用頻率最高的了。這一點(diǎn)大家沒有異議吧,接下來我們由淺入深逐步認(rèn)識一下ArrayList。
1.Java集合的設(shè)計(jì)


? ? ? ? 上圖是Java集合的設(shè)計(jì)層次和java類繼承接口列表,Java設(shè)計(jì)策略先由接口定義集合應(yīng)該具有的功能,接口設(shè)計(jì)完成后在具體類中實(shí)現(xiàn)具有這些功能的類。另外java集合提供了Collections工具類,它已經(jīng)實(shí)現(xiàn)了對集合排序,遍歷等算法,我們可以使用它,快速的完成集合的排序、遍歷等功能。同樣我們今天要聊到的ArrayList他繼承List,擁有Collection接口定義的功能,同樣可以使用Collections工具類對集合進(jìn)行排序、遍歷等。
2.ArrayList源碼分析
? ? ? ? 關(guān)于ArrayList的原理,通過查看package java.util.ArrayList實(shí)現(xiàn),下面截圖部分是ArrayList的兩個構(gòu)造方法,一個是無參數(shù)一個是帶有長度參數(shù)構(gòu)造方法的代碼,閱讀代碼我們可以發(fā)現(xiàn)ArrayList的底層是基于一個Object數(shù)組實(shí)現(xiàn)的。


? ? ? ? 既然知道了ArrayList底層是數(shù)組,故ArrayList擁有數(shù)組同樣特性,比如可以通過索引獲取指定位置的數(shù)據(jù),也可以使用indexOf找到某個對象的索引位置。既然是數(shù)組在初始化創(chuàng)建對象的時候必須指定長度。

3.ArrayList擴(kuò)容原理
? ? ? ? 源碼中指定ArrayList的默認(rèn)初始大小為10的數(shù)組,長度為10的Object數(shù)組很容易就裝滿了,如果增加的元素個數(shù)超過了10個,那么ArrayList底層會新生成一個數(shù)組,長度為原來數(shù)組的1.5倍+1,然后將原數(shù)組的內(nèi)容復(fù)制到新數(shù)組中,并且后續(xù)增加的內(nèi)容都會放到新數(shù)組當(dāng)中,當(dāng)新數(shù)組無法容納增加的元素時,重復(fù)該過程。下圖可以看到ArrayList擴(kuò)容過程

? ? ? ? JVM操作自動擴(kuò)容的時候非常消耗性能,所以在選取使用ArrayList的時候最好事先估算一下它的長度,然后創(chuàng)建對象的時候傳入估算的值。盡量避免ArrayList自動擴(kuò)容,也有人會這樣考慮,初始化ArrayList指定一個足夠大的數(shù)據(jù),雖然避免的自動擴(kuò)容,但是造成內(nèi)存的浪費(fèi)。ArrayList既然是事先分配的數(shù)組,分配的數(shù)組每個項(xiàng)的長度是固定的,那么如何使用ArrayList存放復(fù)雜的對象呢?
4.ArrayList數(shù)據(jù)存儲原理
? ? ? ? ArrayList是一個Object數(shù)組,如果ArrayList存放基本類型,可以直接存放到數(shù)組中,但使用ArrayList存放對象,事先分配的Object無法存放形式和大小各異的對象,那么ArrayList進(jìn)行了優(yōu)化,在ArrayList集合中存放的是對象的引用,而不是對象本身。

? ? ? ? ArrayList巧妙的使用存對象引用的方法解決了復(fù)雜對象的存取問題,這樣的設(shè)計(jì)類似C++的指針,存取的索引地址可以方便方位索引位置的對象信息。
5.ArrayList插入和查詢數(shù)據(jù)原理
? ? ? ? 接下來我們透過ArrayList的具體方法操作,看ArrayList是如何進(jìn)行元素的收集的,因?yàn)锳rrayList底層本質(zhì)是數(shù)組,且實(shí)現(xiàn)了List接口,使用中可以方便調(diào)用List接口定義的add()功能向ArrayList添加內(nèi)容。

? ? ? ? 可以發(fā)現(xiàn),向ArrayList中插入數(shù)據(jù),如果插入的數(shù)據(jù)位于數(shù)組末尾性能非???,但是向數(shù)組中間插入就需要將插入位置后面的所有數(shù)據(jù)順序后移,非常消耗性能,同理remove 集合ArrayList中的一個數(shù)據(jù),也會導(dǎo)致冗長的移位操作,性能消耗得不償失。所以對于ArrayList最好不要輕易改變帶索引的數(shù)據(jù)。
? ? ? ? 知道了數(shù)據(jù)的增刪之后,再來看看ArrayList進(jìn)行數(shù)據(jù)訪問是如何實(shí)現(xiàn)的?下圖是ArrayList定義的獲取值的方法

? ? ? ? 代碼中ArrayList需要傳入索引地址,方法定義通過索引隨機(jī)訪問數(shù)組索引位置的數(shù)據(jù)。它的訪問時間復(fù)雜度O(1),性能非常的快。所以ArrayList適合一次加載多次訪問的數(shù)據(jù)非常的適合。既然ArrayList在查詢方面性能如此適合,那么可以直接使用ArrayList做多線程公共數(shù)據(jù)的查詢服務(wù)嗎?答案是不可以的。
6.ArrayList安全性淺析和應(yīng)對方法
? ? ? ? 因?yàn)锳rrayList是線程不安全的,ArrayList的操作并非是原子性的,通讀ArrayList代碼并沒有實(shí)現(xiàn)線程同步機(jī)制的加鎖約束。ArrayList添加一個元素的時候,需要兩個步驟,第一步在Items[Size]的位置存放此元素,第二步增大Size的值,在單線程運(yùn)行的情況,這兩個步驟是順序執(zhí)行的,互相不會影響。但是如果有兩個線程去操作呢?
? ? ? ? 線程A在0的位置賦了一個值,然后停下來,B線程ArrayList 0的位置又賦了一個值,其實(shí)是重復(fù)在一個位置賦值,然后回到A線程,執(zhí)行Size增加,也就是ArrayList的大小增加了,原來Size是1,現(xiàn)在變成2,然后停下來繼續(xù)執(zhí)行線程B,又增加了一個空間位置,size大小就變成了3,結(jié)果就是0的位置有值,1和2的索引位置都沒有值實(shí)際大小是3,跟想要的結(jié)果0和1賦不同的值,結(jié)果不對。那么如何來解決這個問題呢?
? ? ? ? 在JVM的ArrayList設(shè)計(jì)的時候給出了兩個方法可以讓程序員既能利用ArrayList的隨機(jī)訪問的高效性能,又能避免并發(fā)訪問線程安全問題。
方法一、繼承Arraylist,然后重寫或按需求編寫自己的方法,這些方法要寫成synchronized,在這些synchronized的方法中調(diào)用ArrayList的方法。
方法二、使用Collections.synchronizedList的接口,如下使用:
List list =Collections.synchronizedList(new ArrayList());
方法一可以實(shí)現(xiàn),但是對于程序開發(fā)者來說加大了工作難度,列在這里供參考。為了使用方便推薦直接使用方法二,因?yàn)镃ollections.synchronizedList已經(jīng)實(shí)現(xiàn)了ArrayList的線程安全,所以不用重復(fù)造輪子了。
? ? ? ? 通過今天的分享希望大家對ArrayList有深入的了解。