Java集合—ArrayList底层原理
原文作者:0 errors 0 warnings
原文地址:用大白話告訴你ArrayList的底層原理
目錄
一、數據結構
二、線程安全性
三、繼承關系
四、構造方法
五、add()方法
六、擴容機制
七、set(int index,E element)方法
八、indexOf(Object o)方法
九、get(int index)方法
十、remove(int index)方法
一、數據結構
ArrayList的底層數據結構就是一個數組,數組元素的類型為Object類型,對ArrayList的所有操作底層都是基于數組的。
ArrayList成員變量如下:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {// 版本號private static final long serialVersionUID = 8683452581122892189L;// 缺省容量:當ArrayList的構造方法中沒有顯示指出ArrayList的數組長度時,類內部使用默認缺省時對象數組的容量大小,為10。private static final int DEFAULT_CAPACITY = 10;// 空對象數組:當ArrayList的構造方法中顯示指出ArrayList的數組長度為0時,類內部將EMPTY_ELEMENTDATA 這個空對象數組賦給elemetData數組。private static final Object[] EMPTY_ELEMENTDATA = {};// 缺省空對象數組:當ArrayList的構造方法中沒有顯示指出ArrayList的數組長度時,類內部使用默認缺省時對象數組為DEFAULTCAPACITY_EMPTY_ELEMENTDATA。private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};// 元素數組:ArrayList的底層數據結構,只是一個對象數組,用于存放實際元素,并且被標記為transient,也就意味著在序列化的時候此字段是不會被序列化的。transient Object[] elementData;// 實際元素大小:實際ArrayList中存放的元素的個數,默認時為0個元素。private int size;// 最大數組容量:ArrayList中的對象數組的最大數組容量為Integer.MAX_VALUE – 8。private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; }優缺點:
二、線程安全性
對ArrayList進行添加元素的操作的時候是分兩個步驟進行的,即第一步先在object[size]的位置上存放需要添加的元素;第二步將size的值增加1。由于這個過程在多線程的環境下是不能保證具有原子性的,因此ArrayList在多線程的環境下是線程不安全的。具體舉例說明:在單線程運行的情況下,如果Size = 0,添加一個元素后,此元素在位置 0,而且Size=1;而如果是在多線程情況下,比如有兩個線程,線程 A 先將元素存放在位置0。但是此時 CPU 調度線程A暫停,線程 B 得到運行的機會。線程B也向此ArrayList 添加元素,因為此時 Size 仍然等于 0 (注意哦,我們假設的是添加一個元素是要兩個步驟哦,而線程A僅僅完成了步驟1),所以線程B也將元素存放在位置0。然后線程A和線程B都繼續運行,都增 加 Size 的值。??那好,現在我們來看看 ArrayList 的情況,元素實際上只有一個,存放在位置 0,而Size卻等于 2。這就是“線程不安全”了。
如果非要在多線程的環境下使用ArrayList,就需要保證它的線程安全性,通常有兩種解決辦法:
- 第一,使用synchronized關鍵字;
- 第二,可以用Collections類中的靜態方法synchronizedList();對ArrayList進行調用即可。
三、繼承關系
ArrayList繼承AbstractList抽象父類,實現了List接口(規定了List的操作規范)、RandomAccess(可隨機訪問)、Cloneable(可拷貝)、Serializable(可序列化)。
public ArrayList() { // 無參構造函數,設置元素數組為空 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }四、構造方法
1)無參構造方法:對于無參構造方法,將成員變量elementData的值設為DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
2)int類型參數構造方法:參數為希望的ArrayList的數組的長度,initialCapacity。首先要判斷參數initialCapacity與0的大小關系:如果initialCapacity大于0,則創建一個大小為initialCapacity的對象數組賦給elementData。如果initialCapacity等于0,則將EMPTY_ELEMENTDATA賦給elementData。如果initialCapacity小于0,拋出異常(非法的容量)。
public ArrayList(int initialCapacity) {if (initialCapacity > 0) { // 初始容量大于0this.elementData = new Object[initialCapacity]; // 初始化元素數組} else if (initialCapacity == 0) { // 初始容量為0this.elementData = EMPTY_ELEMENTDATA; // 為空對象數組} else { // 初始容量小于0,拋出異常throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);} }3)Collection<? extends E>類型構造方法:
- 第一步,將參數中的集合轉化為數組賦給elementData;
- 第二步,參數集合是否是空。通過比較size與第一步中的數組長度的大小。
- 第三步,如果參數集合為空,則設置元素數組為空,即將EMPTY_ELEMENTDATA賦給elementData;
- 第四步,如果參數集合不為空,接下來判斷是否成功將參數集合轉化為Object類型的數組,如果轉化成Object類型的數組成功,則將數組進行復制,轉化為Object類型的數組。
五、add()方法
在add()方法中主要完成了三件事:首先確保能夠將希望添加到集合中的元素能夠添加到集合中,即確保ArrayList的容量(判斷是否需要擴容);然后將元素添加到elementData數組的指定位置;最后將集合中實際的元素個數加1。
public boolean add(E e) { // 添加元素ensureCapacityInternal(size + 1); // Increments modCount!!elementData[size++] = e;return true; }六、擴容機制
ArrayList的擴容主要發生在向ArrayList集合中添加元素的時候。由add()方法的分析可知添加前必須確保集合的容量能夠放下添加的元素。主要經歷了以下幾個階段:
- 第一:在add()方法中調用ensureCapacityInternal(size + 1)方法為確保添加元素成功的最小集合容量minCapacity的值。參數為size+1,代表的含義是如果集合添加元素成功后,集合中的實際元素個數。換句話說,集合為了確保添加元素成功,那么集合的最小容量minCapacity應該是size+1。在ensureCapacityInternal方法中,首先判斷elementData是否為默認的空數組,如果是,minCapacity為minCapacity與集合默認容量大小中的較大值。
- 第二,調用ensureExplicitCapacity(minCapacity)方法為了確保添加元素成功是否需要對現有的元素數組進行擴容。首先將結構性修改計數器加一;然后判斷minCapacity與當前元素數組的長度的大小,如果minCapacity比當前元素數組的長度的大小大的時候需要擴容,進入第三階段。
- 第三,如果需要對現有的元素數組進行擴容,則調用grow(minCapacity)方法,參數minCapacity表示集合為了確保添加元素成功的最小容量。在擴容的時候,首先將原元素數組的長度增大1.5倍(oldCapacity + (oldCapacity >> 1)),然后對擴容后的容量與minCapacity進行比較:① 新容量小于minCapacity,則將新容量設為minCapacity;②新容量大于minCapacity,則指定新容量。最后將舊數組拷貝到擴容后的新數組中。
?
七、set(int index,E element)方法
set(int index, E element)方法的作用是指定下標索引處的元素的值。在ArrayList的源碼實現中,方法內首先判斷傳遞的元素數組下標參數是否合法,然后將原來的值取出,設置為新的值,將舊值作為返回值返回。
public E set(int index, E element) {// 檢驗索引是否合法rangeCheck(index);// 舊值E oldValue = elementData(index);// 賦新值elementData[index] = element;// 返回舊值return oldValue; }八、indexOf(Object o)方法
indexOf(Object o)方法的作用是從頭開始查找與指定元素相等的元素,如果找到,則返回找到的元素在元素數組中的下標,如果沒有找到返回-1。與該方法類似的是lastIndexOf(Object o)方法,該方法的作用是從尾部開始查找與指定元素相等的元素。查看該方法的源碼可知,該方法從需要查找的元素是否為空的角度分為兩種情況分別討論。這也意味著該方法的參數可以是null元素,也意味著ArrayList集合中能夠保存null元素。方法實現的邏輯也比較簡單,直接循環遍歷元素數組,通過equals方法來判斷對象是否相同,相同就返回下標,找不到就返回-1。這也解釋了為什么要把情況分為需要查找的對象是否為空兩種情況討論,不然的話空對象調用equals方法則會產生空指針異常。
// 從首開始查找數組里面是否存在指定元素 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; }九、get(int index)方法
get(int index)方法是返回指定下標處的元素的值。get函數會檢查索引值是否合法(只檢查是否大于size,而沒有檢查是否小于0)。如果所引致合法,則調用elementData(int index)方法獲取值。在elementData(int index)方法中返回元素數組中指定下標的元素,并且對其進行了向下轉型。
public E get(int index) {// 檢驗索引是否合法rangeCheck(index);return elementData(index); } E elementData(int index) {return (E) elementData[index]; }十、remove(int index)方法
remove(int index)方法的作用是刪除指定下標的元素。在該方法的源碼中,將指定下標后面一位到數組末尾的全部元素向前移動一個單位,并且把數組最后一個元素設置為null,這樣方便之后將整個數組不再使用時,會被GC,可以作為小技巧。而需要移動的元素個數為:size-index-1。
public E remove(int index) {// 檢查索引是否合法rangeCheck(index);modCount++;E oldValue = elementData(index);// 需要移動的元素的個數int numMoved = size - index - 1;if (numMoved > 0)System.arraycopy(elementData, index+1, elementData, index,numMoved);// 賦值為空,有利于進行GCelementData[--size] = null; // 返回舊值return oldValue; }總結
以上是生活随笔為你收集整理的Java集合—ArrayList底层原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 重要接口—NavigableSet接口
- 下一篇: Java集合—PriorityQueue