java集合原始碼分析(三)--ArrayList原始碼
吐槽
週末啊,冷啊啊啊啊,然後怕自己週六中午睡起來都晚上了,就不睡午覺了去實驗室看下ArrayList的原始碼。
之前自己學集合只是簡單的看了下用法,寫專案的時候雖然用這塊但是也沒仔細看下這塊到底咋實現的。
ArrayList的基本功能
首先這個貨是個陣列
陣列就是存放東西的一個倉庫
但是這個和普通的普通的陣列還是有區別的
它的特殊的地方就是可以動態的新增或者減少這個數組裡面的元素emmmmm//當然也是有限制的不可能無限放東西
我們今天要看的就是ArrayList的幾個問題
- Arraylist動態新增或者減少怎麼樣實現的?
- 它的執行緒的安全性?
- 它的最大的容量是多少?
- java中Array和ArrayList區別?
ArrayList類的介紹
首先去文件上看下這塊的繼承關係
發現這個貨繼承了AbstractList然後實現了四個個介面List, RandomAccess, Cloneable, Serializable
根據我們看他繼承的類和試下的介面看到他有一下的能力
- ArrayList 繼承了AbstractList,實現了List。它是一個數組佇列,提供了相關的新增、刪除、修改、遍歷等功能
- ArrayList 實現了RandmoAccess介面,即提供了隨機訪問功能。RandmoAccess是java中用來被List實現,為List提供快速訪問功能的。
- ArrayList 實現了Cloneable介面,即覆蓋了函式clone(),能被克隆。
- ArrayList 實現java.io.Serializable介面,這意味著ArrayList支援序列化,能通過序列化去傳輸。
我們還是去看下這塊的原始碼吧233也不是很長,我們一部分一部分看
類的成員變數介紹
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
//可序列化的版本號
private static final long serialVersionUID = 8683452581122892189 L;
//預設的陣列大小為10 重點
private static final int DEFAULT_CAPACITY = 10;
//例項化一個空的陣列 當用戶指定的ArrayList為0的時候 返回這個
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
//一個空陣列例項 當用戶沒有指定 ArrayList 的容量時(即呼叫無參建構函式),返回的是該陣列==>剛建立一個 ArrayList 時,其內資料量為 0。
//當用戶第一次新增元素時,該陣列將會擴容,變成預設容量為 10(DEFAULT_CAPACITY) 的一個數組===>通過 ensureCapacityInternal() 實現
//它與 EMPTY_ELEMENTDATA 的區別就是:該陣列是預設返回的,而後者是在使用者指定容量為 0 時返回
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
//存放List元素的陣列 儲存了新增到ArrayList中的元素。實際上,elementData是個動態陣列 ArrayList基於陣列實現,用該陣列儲存資料, ArrayList 的容量就是該陣列的長度
//該值為 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 時,當第一次新增元素進入 ArrayList 中時,陣列將擴容值 DEFAULT_CAPACITY(10)
transient Object[] elementData;
//List中元素的數量 存放List元素的陣列長度可能相等,也可能不相等
private int size;
//這個數字就是最大存放的大小emmmmmm
private static final int MAX_ARRAY_SIZE = 2147483639;
裡面也蠻清楚的有兩個總要的物件:
elementData 陣列 "Object[]型別的陣列,後面的初始化,其他方面有很重要的用處
size 這個是動態陣列的實際大小
構造方法
有三個
public ArrayList(int var1) {
if (var1 > 0) {
//建立一樣大的elementData陣列
this.elementData = new Object[var1];
} else {
if (var1 != 0) {
//傳入的引數為負數時候 報錯
throw new IllegalArgumentException("Illegal Capacity: " + var1);
}
//初始化這個為空的陣列
this.elementData = EMPTY_ELEMENTDATA;
}
}
//構造方法 無參 陣列緩衝區 elementData = {}, 長度為 0
//當元素第一次被加入時,擴容至預設容量 10
public ArrayList() {
//初始化這個為空的陣列
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//構造方法,引數為集合元素
public ArrayList(Collection<? extends E> var1) {
//將集合元素轉換為陣列,然後給elementData陣列
this.elementData = var1.toArray();
if ((this.size = this.elementData.length) != 0) {
//如果不是object型別的陣列,轉換成object型別的陣列
if (this.elementData.getClass() != Object[].class) {
this.elementData = Arrays.copyOf(this.elementData, this.size, Object[].class);
}
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
構造方法裡面這塊就發現elementData陣列真的是作用很大啊
我們發現這塊它被修飾的時候是用transient修飾的
transient是幹嘛的啊
當物件被序列化時(寫入位元組序列到目標檔案)時,transient阻止例項中那些用此關鍵字宣告的變數持久化;當物件被反序列化時(從原始檔讀取位元組序列進行重構),這樣的例項變數值不會被持久化和恢復。例如,當反序列化物件——資料流(例如,檔案)可能不存在時,原因是你的物件中存在型別為java.io.InputStream的變數,序列化時這些變數引用的輸入流無法被開啟。
那麼為什麼ArrayList裡面的elementData為什麼要用transient來修飾?
因為ArrayList不能序列化和反序列化嗎?肯定不是,是因為elementData裡面不是所有的元素都有資料,因為容量的問題,elementData裡面有一些元素是空的,這種是沒有必要序列化的。ArrayList的序列化和反序列化依賴writeObject和readObject方法來實現。可以避免序列化空的元素。
序列化的
存放元素和改變容量的方法
//改變陣列的長度,使長度和List的size相等。
//集合中元素個數的size和表示集合容量的elementData.length可能不同,在不太需要增加//集合元素的情況下容量有浪費,可以使用trimToSize方法減小elementData的大小
public void trimToSize() {
++this.modCount;//繼承自AbstractList中的欄位,表示陣列修改的次數,陣列每修改一次,就要增加modCount
if (this.size < this.elementData.length) {
this.elementData = this.size == 0 ? EMPTY_ELEMENTDATA : Arrays.copyOf(this.elementData, this.size);
}
}
//確定ArrayList的容量
//判斷當前elementData是否是EMPTY_ELEMENTDATA,若是設定長度為10
public void ensureCapacity(int var1) {
int var2 = this.elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA ? 0 : 10;
if (var1 > var2) {
//需要擴充
this.ensureExplicitCapacity(var1);
}
}
//最小擴充容量,預設是 10
//如果elementData為空的時候 看下長度和10比較的結果,找最大的
//判斷是不是空的ArrayList,如果是的最小擴充容量10,否則最小擴充量為0
//上面無參建構函式建立後,當元素第一次被加入時,擴容至預設容量 10,就是靠這句程式碼
private void ensureCapacityInternal(int var1) {
// 若使用者指定的最小容量 > 最小擴充容量,則以使用者指定的為準,否則還是 10
if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
var1 = Math.max(10, var1);
}
//擴容
this.ensureExplicitCapacity(var1);
}
//minCapacity和預設大小(10)比較,如果需要擴大容量,繼續呼叫
private void ensureExplicitCapacity(int var1) {
++this.modCount;
if (var1 - this.elementData.length > 0) {
this.grow(var1);
}
}
//擴容的操作
private void grow(int var1) {
// 防止溢位程式碼
int var2 = this.elementData.length;
//容量擴容為之前的1.5倍
int var3 = var2 + (var2 >> 1);
//對新的容量進行判斷
// 若 newCapacity 依舊小於 minCapacity
if (var3 - var1 < 0) {
var3 = var1;
}
// 若 newCapacity 大於最大儲存容量,則進行大容量分配
if (var3 - 2147483639 > 0) {
var3 = hugeCapacity(var1);
}
//複製舊元素到新的陣列上面
this.elementData = Arrays.copyOf(this.elementData, var3);
}
//這個就是判斷大小的
private static int hugeCapacity(int var0) {
if (var0 < 0) {
//如果傳入的小於0的話 報錯
throw new OutOfMemoryError();
} else {
//大於0的話 如果大於 ((2^31)-1) = 2147483647-8 = 2147483639
return var0 > 2147483639 ? 2147483647 : 2147483639;
}
}
我們看到這塊的程式碼
擴容的時候利用位運算把容量擴充到之前的1.5倍
比如說用預設的構造方法,初始容量被設定為10。當ArrayList中的元素超過10個以後,會重新分配記憶體空間,使陣列的大小增長到16。
可以通過除錯看到動態增長的數量變化:10->16->25->38->58->88->…
將ArrayList的預設容量設定為4。當ArrayList中的元素超過4個以後,會重新分配記憶體空間,使陣列的大小增長到7。
可以通過除錯看到動態增長的數量變化:4->7->11->17->26->…
公式就是 新的容量 = (舊的容量*3)/2 +1;
然後還要檢測下大小,不能超出最大的範圍
那那那這個Java中ArrayList最大容量是多少啊?
大約是8G
看下原始碼發現是2147483639 這個數字emmmmmmmmm
看下別人的原始碼是private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;好像是我看的原始碼版本問題
Integer.MAX_VALUE 是 2的31次方減一
為什麼要減8,,,原因好像是因為只是為了避免一些機器記憶體溢位, -8 是為了減少出錯的機率,虛擬機器在陣列中保留了一些頭資訊。避免記憶體溢位。
增加
//這個是 不指定位置的話 新增到陣列末尾
public boolean add(E var1) {
this.ensureCapacityInternal(this.size + 1);
this.elementData[this.size++] = var1;
return true;
}
//指定位置的情況
public void add(int var1, E var2) {
//檢驗下是否插入的位置在陣列容量範圍內
this.rangeCheckForAdd(var1);
//檢查是否需要擴容
this.ensureCapacityInternal(this.size + 1);
System.arraycopy(this.elementData, var1, this.elementData, var1 + 1, this.size - var1);
//騰出新空間新增元素
this.elementData[var1] = var2;
//修改陣列內元素的數量
++this.size;
}
private void rangeCheckForAdd(int var1) {
if (var1 > this.size || var1 < 0) {
throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1));
}
}
//檢驗下是否插入的位置在陣列容量範圍內
private void rangeCheckForAdd(int var1) {
if (var1 > this.size || var1 < 0) {
throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1));
}
}
//檢測是否要擴容
private void ensureCapacityInternal(int var1) {
if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
var1 = Math.max(10, var1);
}
this.ensureExplicitCapacity(var1);
}
private void ensureExplicitCapacity(int var1) {
++this.modCount;
if (var1 - this.elementData.length > 0) {
this.grow(var1);
}
}
查詢
它這個有兩個查詢
普通查詢
逆序查詢 思路也蠻清楚的
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
刪除
這個和陣列的刪除類似的emmmmmmmmm
都是把指定位置的元素刪除後,它後面的元素統一向前移動一位
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;
E oldValue = (E) elementData[index];
//要移動的長度
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 將最後一個元素置空
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
//移除指定的第一個元素
// 如果list中不包含這個元素,這個list不會改變
// 如果包含這個元素,index 之後的所有元素依次左移一位
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
//快速刪除下標第index的元素
private void fastRemove(int index) {
modCount++;//這個地方改變了modCount的值了
int numMoved = size - index - 1;//移動的個數
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; //將最後一個元素清除
}
ArrayList執行緒不安全的原因
雖然之前一直背的是ArrayList的執行緒不安全,但是還是不知道是為什麼,作業系統課上對這塊的理解就是當兩個執行緒請求資料的時候,可能一個執行緒不小心把另一個執行緒的裡面的東西改了,這塊又和計算機的儲存有關了
一般來說,,,遇到 變數++這種的,,,很容易遇到執行緒不安全的問題
我們來看下這塊的執行緒不安全的原因
在之前的成員變數裡面我們說過裡面有兩個貨特別重要
transient Object[] elementData;
private int size;
elementData是個動態陣列 ArrayList基於陣列實現,用該陣列儲存資料, ArrayList 的容量就是該陣列的長度
個size變數用來儲存當前陣列中已經添加了多少元素
外面再進行add()操作時候的一系列原始碼
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
之前上面分析了這幾個函數了的 這塊就不講了
主體思路就是我要新增一個元素,先進行判斷一下,看下里面空間夠不夠,不夠的話我就擴容什麼的,然後再把這個元素加進去。裡面其實就是兩部操作
1判斷elementData陣列容量是否滿足需求
2在elementData對應位置上設定值
所以,這塊就是出現執行緒不安全的第一個地方,在多個執行緒進行add操作時可能會導致elementData陣列越界。具體邏輯如下:
- 列表大小為9,即size=9
- 執行緒A開始進入add方法,這時它獲取到size的值為9,呼叫ensureCapacityInternal方法進行容量判斷。 執行緒A開始進入add方法,這時它獲取到size的值為9,呼叫ensureCapacityInternal方法進行容量判斷
- 執行緒B此時也進入add方法,它獲取到size的值也為9,也開始呼叫ensureCapacityInternal方法。 執行緒B此時也進入add方法,它獲取到size的值也為9,也開始呼叫ensureCapacityInternal方法
- 執行緒A發現需求大小為10,而elementData的大小就為10,可以容納。於是它不再擴容,返回。執行緒A發現需求大小為10,而elementData的大小就為10,可以容納。於是它不再擴容,返回
- 執行緒B也發現需求大小為10,也可以容納,返回。 執行緒A開始進行設定值操作, elementData[size++] = e執行緒B也發現需求大小為10,也可以容納,返回
- 執行緒A開始進行設定值操作, elementData[size++] = e執行緒A開始進行設定值操作, elementData[size++] = e操作。此時size變為10。
- 執行緒B也開始進行設定值操作,它嘗試設定elementData[10] =執行緒B也開始進行設定值操作,它嘗試設定elementData[10] =e,而elementData沒有進行過擴容,它的下標最大為9。於是此時會報出一個數組越界的異常ArrayIndexOutOfBoundsException.
然後第二個執行緒不安全的地方就是 elementData[size++] = e 設定值的操作同樣會導致執行緒不安全。這塊不是原子操作,所以會髒資料,他由兩步操作構成
1elementData[size] = e;
2size = size + 1;
所以這塊也就是在多執行緒的時候執行的話出現問題了
- 列表大小為0,即size=0
- 執行緒A開始新增一個元素,值為A。此時它執行第一條操作,將A放在了elementData下標為0的位置上。執行緒A開始新增一個元素,值為A。此時它執行第一條操作,將A放在了elementData下標為0的位置上。
- 接著執行緒B剛好也要開始新增一個值為B的元素,且走到了第一步操作。此時執行緒B獲取到size的值依然為0,於是它將B也放在了elementData下標為0的位置上。
- 接著執行緒B剛好也要開始新增一個值為B的元素,且走到了第一步操作。此時執行緒B獲取到size的值依然為0,於是它將B也放在了elementData下標為0的位置上。接著執行緒B剛好也要開始新增一個值為B的元素,且走到了第一步操作。此時執行緒B獲取到size的值依然為0,於是它將B也放在了elementData下標為0的位置上。
- 執行緒A開始將size的值增加為1執行緒A開始將size的值增加為1
- 執行緒B開始將size的值增加為2執行緒B開始將size的值增加為2
這樣執行緒AB執行完畢後,理想中情況為size為2,elementData下標0的位置為A,下標1的位置為B。而實際情況變成了size為2,elementData下標為0的位置變成了B,下標1的位置上什麼都沒有。並且後續除非使用set方法修改此位置的值,否則將一直為null,因為size為2,新增元素時會從下標為2的位置上開始。
如何使ArrayList執行緒安全
- 繼承Arraylist,然後重寫或按需求編寫自己的方法,這些方法要寫成synchronized,在這些synchronized的方法中呼叫ArrayList的方法。
- List list = Collections.synchronizedList(new ArrayList());
當多執行緒訪問這些容器類時,可能會出現資料同步導致的問題,java的工具類java.util.Collections提供了將非同步物件轉換為同步物件的方法
當多執行緒訪問這些容器類時,可能會出現資料同步導致的問題,java的工具類java.util.Collections提供了將非同步物件轉換為同步物件的方法
自己對ArrayList的語言總結
因為昨天學長給我們模擬面試,然後我發現昨天早上才把原始碼看了,但是自己還是講東西給面試官還是很卡頓。所以我覺的把每次學的東西自己組織語言過一遍,然後再給別人講就會好一點。
問:簡單介紹下ArrayList
答:ArrayList是以陣列實現,可以自動擴容的動態陣列,當超出限制的時候會增加50%的容量,用ystem.arraycopy()複製到新的陣列,因此最好能給出陣列大小的預估值。預設第一次插入元素時建立大小為10的陣列。arrayList的效能很高效,不論是查詢和取值很迅速,但是插入和刪除效能較差,該集合執行緒不安全。
問:ArrayList的自動擴容怎麼樣實現的
關鍵字:elementData size ensureCapacityInternal
答:每次在add()一個元素時,arraylist都需要對這個list的容量進行一個判斷。如果容量夠,直接新增,否則需要進行擴容。在1.8 arraylist這個類中,擴容呼叫的是grow()方法。
在核心grow方法裡面,首先獲取陣列原來的長度,然後新增加容量為之前的1.5倍。隨後,如果新容量還是不滿足需求量,直接把新容量改為需求量,然後再進行最大化判斷。
通過grow()方法中呼叫的Arrays.copyof()方法進行對原陣列的複製,在通過呼叫System.arraycopy()方法進行復制,達到擴容的目的。
問:ArrayList的構造方法過程答:ArrayList裡面有三種構造方法,第一種:無參的構造方法 先將陣列為空,第一次加入的時候 然後擴充預設為10, 第二種是有參的構造方法 ,直接建立這個陣列 第三種是傳入集合元素,先將集合元素轉換為陣列,把不是object的陣列轉化為object陣列。
問:ArrayList可以無限擴大嗎?答:不能,大於是8G,因為在ArrayList擴容的時候,有個界限判斷。 private static final int MAX_ARRAY_SIZE = 2147483639,2的31次方減一然後減8,-8 是為了減少出錯的機率,虛擬機器在陣列中保留了一些頭資訊。避免記憶體溢位。