詳解ArrayList的擴(kuò)容機(jī)制
一、ArrayList 了解過嗎?它是啥?有啥用?
眾所周知,Java 集合框架擁有兩大接口 Collection
和 Map
,其中,Collection
麾下三生子 List
、Set
和 Queue
。ArrayList
就實(shí)現(xiàn)了 List
接口,其實(shí)就是一個數(shù)組列表,不過作為 Java 的集合框架,它只能存儲對象引用類型,也就是說當(dāng)我們需要裝載的數(shù)據(jù)是諸如 int
、float
等基本數(shù)據(jù)類型的時候,必須把它們轉(zhuǎn)換成對應(yīng)的包裝類。
ArrayList
的底層實(shí)現(xiàn)是一個 Object
數(shù)組:
既然它是基于數(shù)組實(shí)現(xiàn)的,數(shù)組在內(nèi)存空間中是連續(xù)分配的,那必然查詢速率非???,不過當(dāng)然也肯定逃不過增刪效率低的缺陷。
另外,和 ArrayList
一樣同樣實(shí)現(xiàn)了 List
接口的、我們比較常用的還有 LinkedList
。LinkedList
比較特殊,它不僅實(shí)現(xiàn)了 List
接口,還實(shí)現(xiàn)了 Queue
接口,所以你可以看見 LinkedList
經(jīng)常被當(dāng)作隊(duì)列使用:
Queue<Integer> queue = new LinkedList<>();
LinkedList
人如其名,它的底層自然是基于鏈表的,而且還是個雙向鏈表。鏈表的特性和數(shù)組正好是反的,由于沒有索引,所以查詢效率低,但是增刪速度快。
二、ArrayList 如何指定底層數(shù)組大小的
OK,首先,既然咱真正存儲數(shù)據(jù)的地方是數(shù)組,那我們初始化 ArrayList
的時候自然要給數(shù)組分配一個大小,開辟一個內(nèi)存空間。我們先來看看 ArrayList
的無參構(gòu)造函數(shù):
可以看到,它為底層的 Object
數(shù)組也就是 elementData 賦值了一個默認(rèn)的空數(shù)組 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
。也就是說,使用無參構(gòu)造函數(shù)初始化 ArrayList
后,它當(dāng)時的數(shù)組容量為 0 。
這給咱初始化一個容量為 0 的數(shù)組有啥用?啥也存不了???別急,如果使用了無參構(gòu)造函數(shù)來初始化 ArrayList
, 只有當(dāng)我們真正對數(shù)據(jù)進(jìn)行添加操作 add
時,才會給數(shù)組分配一個默認(rèn)的初始容量 DEFAULT_CAPACITY = 10
??聪聢D:
說完了無參構(gòu)造,ArrayList
的有參構(gòu)造函數(shù)就是中規(guī)中矩了,按照用戶傳入的大小開辟數(shù)組空間:
三、數(shù)組的大小一旦被規(guī)定就無法改變
ArrayList 是怎么對底層數(shù)組進(jìn)行擴(kuò)容的?
ArrayList
的底層實(shí)現(xiàn)是 Object
數(shù)組,我們知道,數(shù)組的大小一旦被規(guī)定就無法改變。那如果我們不斷的往里面添加數(shù)據(jù)的話,ArrayList
是如何進(jìn)行擴(kuò)容的呢?或者說 ArrayList 是如何實(shí)現(xiàn)存放任意數(shù)量對象的呢?
OK,擴(kuò)容發(fā)生在啥時候?那肯定是我們往數(shù)組中新加入一個元素但是發(fā)現(xiàn)數(shù)組滿了的時候。沒錯,我們?nèi)?add
方法中看看 ArrayList
是怎么做擴(kuò)容的:
ensureExplicitCapacity
判斷是否需要進(jìn)行擴(kuò)容,很顯然,grow
方法是擴(kuò)容的關(guān)鍵:
說實(shí)話,別的都不用看了,看上面圖中的黃色框框就知道 ArrayList
是怎么擴(kuò)容的了:擴(kuò)容后的數(shù)組長度 = 當(dāng)前數(shù)組長度 + 當(dāng)前數(shù)組長度 / 2。最后使用 Arrays.copyOf
方法直接把原數(shù)組中的數(shù)組 copy 過來,需要注意的是,Arrays.copyOf
方法會創(chuàng)建一個新數(shù)組然后再進(jìn)行拷貝。
舉個例子畫個圖來演示一下:
四、ArrayList 具體是怎么添加數(shù)據(jù)的
OK,add
方法我們剛剛講了一半,添加數(shù)據(jù)前會先判斷一下是否需要擴(kuò)容,真正的添加數(shù)據(jù)的操作在下半部分:
先講下 add(int index, E element)
這個方法的含義,就是在指定索引 index 處插入元素 element。比如說 ArrayList.add(0, 3)
,意思就是在頭部插入元素 3。
再來看看 add
方法的核心 System.arraycopy
,這個方法有 5 個參數(shù):
- elementData:源數(shù)組
- index:從源數(shù)組中的哪個位置開始復(fù)制
- elementData:目標(biāo)數(shù)組
- index + 1:復(fù)制到目標(biāo)數(shù)組中的哪個位置
- size - index:要復(fù)制的源數(shù)組中數(shù)組元素的數(shù)量
解釋一下上面代碼中 arraycopy
的意思,舉個例子,我們想要在 index = 5 的位置插入元素,首先,我們會復(fù)制一遍源數(shù)組 elementData(這里我們稱復(fù)制的數(shù)組為新數(shù)組吧),然后把源數(shù)組中從 index = 5 的位置開始到數(shù)組末尾的元素,放到新數(shù)組的 index + 1 = 6 的位置上:
于是,這就給我們要新增的元素騰出了位置,然后在新數(shù)組 index = 5 的位置放入元素 element 就完成了添加的操作:
顯然,不用多說,ArrayList 的將數(shù)據(jù)插入到指定位置的操作性能非常低下,因?yàn)橐_辟新數(shù)組復(fù)制元素啊,要是涉及到擴(kuò)容那就更慢了。
另外,ArrayList
還內(nèi)置了一個直接在末尾添加元素的 add
方法,不用復(fù)制數(shù)組,直接 size ++ 就好,這個方法應(yīng)該是我們最常使用的:
五、ArrayList 又是如何刪除數(shù)據(jù)的呢
Ctrl + F 找到 remove
方法,就這?和添加一個道理,也是復(fù)制數(shù)組
舉個例子,假設(shè)我們要刪除數(shù)組的 index = 5 的元素,首先,我們會復(fù)制一遍源數(shù)組,然后把源數(shù)組中從 index + 1 = 6 的位置開始到數(shù)組末尾的元素,放到新數(shù)組的 index = 5 的位置上:
也就是說 index = 5 的元素直接被覆蓋掉了,給了你被刪除的感覺。同樣的,它的效率自然也是十分低下的
六、ArrayList 是線程安全的嗎?不安全的表現(xiàn)
ArrayList
和 LinkedList
都不是線程安全的,我們以在末尾添加元素的 add
方法為例,來看看 ArrayList
線程不安全的表現(xiàn)是啥:
黃色框里的并不是一個原子操作,它由兩步操作構(gòu)成:
elementData[size] = e; size = size + 1;
在單線程執(zhí)行這兩條代碼時,那當(dāng)然沒有任何問題,但是當(dāng)多線程環(huán)境下執(zhí)行時,可能就會發(fā)生一個線程添加的值覆蓋另一個線程添加的值。舉個例子:
- 假設(shè) size = 0,我們要往這個數(shù)組的末尾添加元素
- 線程 A 開始添加一個元素,值為 A。此時它執(zhí)行第一條操作,將 A 放在了數(shù)組 elementData 下標(biāo)為 0 的位置上
- 接著線程 B 剛好也要開始添加一個值為 B 的元素,且走到了第一步操作。此時線程 B 獲取到的 size 值依然為 0,于是它將 B 也放在了 elementData 下標(biāo)為 0 的位置上
- 線程 A 開始增加 size 的值,size = 1
- 線程 B 開始增加 size 的值,size = 2
這樣,線程 A、B 都執(zhí)行完畢后,理想的情況應(yīng)該是 size = 2,elementData[0] = A,elementData[1] = B。而實(shí)際情況變成了 size = 2,elementData[0] = B(線程 B 覆蓋了線程 A 的操作),下標(biāo) 1 的位置上什么都沒有。并且后續(xù)除非我們使用 set 方法修改下標(biāo)為 1 的值,否則這個位置上將一直為 null,因?yàn)樵谀┪蔡砑釉貢r將會從 size = 2 的位置上開始。
上段代碼驗(yàn)證下:
結(jié)果和我們分析的一樣:
ArrayList
的線程安全版本是 Vector
,它的實(shí)現(xiàn)很簡單,就是把所有的方法統(tǒng)統(tǒng)加上 synchronized
:
既然它需要額外的開銷來維持同步鎖,所以理論上來說它要比 ArrayList
要慢。
七、為什么線程不安全還要用它呢
因?yàn)樵诖蠖鄶?shù)場景中,查詢的情況居多,不會涉及太頻繁的增刪。那如果真的涉及頻繁的增刪,可以使用LinkedList
,底層鏈表實(shí)現(xiàn),為增刪而生。而如果你非得保證線程安全那就使用 Vector
。當(dāng)然實(shí)際開發(fā)中使用最多的還是 ArrayList
,雖然線程不安全、增刪效率低,但是查詢效率高啊。
以上就是詳解ArrayList的擴(kuò)容機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于ArrayList 擴(kuò)容機(jī)制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
spring cloud升級到spring boot 2.x/Finchley.RELEASE遇到的坑
這篇文章主要介紹了spring cloud升級到spring boot 2.x/Finchley.RELEASE遇到的坑,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08Java面試突擊為什么要用HTTPS及它的優(yōu)點(diǎn)
這篇文章主要介紹了Java面試突擊為什么要用HTTPS及它的優(yōu)點(diǎn),文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-07-07Java 基礎(chǔ)全面講解StringBuffer類的使用
當(dāng)對字符串進(jìn)行修改的時候,需要使用 StringBuffer 和 StringBuilder類,和String類不同的是,StringBuffer和 StringBuilder類的對象能夠被多次的修改,并且不產(chǎn)生新的未使用對象2022-01-01spring事務(wù)之事務(wù)掛起和事務(wù)恢復(fù)源碼解讀
這篇文章主要介紹了spring事務(wù)之事務(wù)掛起和事務(wù)恢復(fù)源碼解讀,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-11-11controller函數(shù)中參數(shù)列表使用多個@RequestBody問題
這篇文章主要介紹了controller函數(shù)中參數(shù)列表使用多個@RequestBody問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04springboot整合mybatis-plus基于注解實(shí)現(xiàn)一對一(一對多)查詢功能
這篇文章主要介紹了springboot整合mybatis-plus基于純注解實(shí)現(xiàn)一對一(一對多)查詢功能,因?yàn)楸救瞬捎玫氖莝pring-boot進(jìn)行開發(fā),本身springboot就提倡采用不用配置自動配置的方式,所以真心希望mybatis(不是mybatis-plus)這點(diǎn)需要繼續(xù)努力2021-09-09