深入理解ArrayList的底層原理

一.ArrayList 的數據結構
ArrayList的底層數據結構就是一個數組,數組元素的類型爲Object類型,對ArrayList的所有操作底層都是基於數組的。
 
二.ArrayList的線程安全性
    對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的繼承關係
ArrayList繼承AbstractList抽象父類,實現了List接口(規定了List的操作規範)、RandomAccess(可隨機訪問)、Cloneable(可拷貝)、Serializable(可序列化)。
 

四.ArrayList的主要成員變量

  • private static final int DEFAULT_CAPACITY = 10;

當ArrayList的構造方法中沒有顯示指出ArrayList的數組長度時,類內部使用默認缺省時對象數組的容量大小,爲10。
 

  • private static final Object[] EMPTY_ELEMENTDATA = {};

當ArrayList的構造方法中顯示指出ArrayList的數組長度爲0時,類內部將EMPTY_ELEMENTDATA 這個空對象數組賦給elemetData數組。
 

  • private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

當ArrayList的構造方法中沒有顯示指出ArrayList的數組長度時,類內部使用默認缺省時對象數組爲DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
 

  • transient Object[] elemetData;

ArrayList的底層數據結構,只是一個對象數組,用於存放實際元素,並且被標記爲transient,也就意味着在序列化的時候此字段是不會被序列化的。
 

  • private int size;

實際ArrayList中存放的元素的個數,默認時爲0個元素。
 

  • private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE – 8;

ArrayList中的對象數組的最大數組容量爲Integer.MAX_VALUE – 8。
 

[Java] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 版本號
    private static final long serialVersionUID = 8683452581122892189L;
    // 缺省容量
    private static final int DEFAULT_CAPACITY = 10;
    // 空對象數組
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 缺省空對象數組
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 元素數組
    transient Object[] elementData;
    // 實際元素大小,默認爲0
    private int size;
    // 最大數組容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}



五.ArrayList的構造方法
1.無參構造方法
對於無參構造方法,將成員變量elementData的值設爲DEFAULTCAPACITY_EMPTY_ELEMENTDATA。

[Java] 純文本查看 複製代碼
1
2
3
4
public ArrayList() {
        // 無參構造函數,設置元素數組爲空
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}


2.int類型參數構造方法
參數爲希望的ArrayList的數組的長度,initialCapacity。首先要判斷參數initialCapacity與0的大小關係:

如果initialCapacity大於0,則創建一個大小爲initialCapacity的對象數組賦給elementData。

如果initialCapacity等於0,則將EMPTY_ELEMENTDATA賦給elementData。

如果initialCapacity小於0,拋出異常(非法的容量)。

[Java] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) { // 初始容量大於0
        this.elementData = new Object[initialCapacity]; // 初始化元素數組
    } else if (initialCapacity == 0) { // 初始容量爲0
        this.elementData = EMPTY_ELEMENTDATA; // 爲空對象數組
    } else { // 初始容量小於0,拋出異常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
    }
}


3.Collection<? extends E> 類型構造方法第一步,將參數中的集合轉化爲數組賦給elementData;

第二步,參數集合是否是空。通過比較size與第一步中的數組長度的大小。

第三步,如果參數集合爲空,則設置元素數組爲空,即將EMPTY_ELEMENTDATA賦給elementData;

第四步,如果參數集合不爲空,接下來判斷是否成功將參數集合轉化爲Object類型的數組,如果轉化成Object類型的數組成功,則將數組進行復制,轉化爲Object類型的數組。

[Java] 純文本查看 複製代碼
1
2
3
4
5
6
7
8
9
public ArrayList(Collection<? extends E> c) { // 集合參數構造函數
    elementData = c.toArray(); // 轉化爲數組
    if ((size = elementData.length) != 0) { // 參數爲非空集合
        if (elementData.getClass() != Object[].class) // 是否成功轉化爲Object類型數組
            elementData = Arrays.copyOf(elementData, size, Object[].class); // 不爲Object數組的話就進行復制
    } else { // 集合大小爲空,則設置元素數組爲空
        this.elementData = EMPTY_ELEMENTDATA;
    }
}



六.ArrayList的add()方法
在add()方法中主要完成了三件事:首先確保能夠將希望添加到集合中的元素能夠添加到集合中,即確保ArrayList的容量(判斷是否需要擴容);然後將元素添加到elementData數組的指定位置;最後將集合中實際的元素個數加1。

[Java] 純文本查看 複製代碼
1
2
3
4
5
public boolean add(E e) { // 添加元素
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}



七.ArrayList的擴容機制
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,則指定新容量。最後將舊數組拷貝到擴容後的新數組中。

[Java] 純文本查看 複製代碼
1
2
3
4
5
6
7
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 判斷元素數組是否爲空數組
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 取較大值
    }
         
    ensureExplicitCapacity(minCapacity);
}



 

[Java] 純文本查看 複製代碼
1
2
3
4
5
6
private void ensureExplicitCapacity(int minCapacity) {
    // 結構性修改加1
        modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}


 

[Java] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
private void grow(int minCapacity) {
    int oldCapacity = elementData.length; // 舊容量
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量爲舊容量的1.5倍
    if (newCapacity - minCapacity < 0) // 新容量小於參數指定容量,修改新容量
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0) // 新容量大於最大容量
        newCapacity = hugeCapacity(minCapacity); // 指定新容量
    // 拷貝擴容
    elementData = Arrays.copyOf(elementData, newCapacity);
}







八、ArrayList的set(int index,E element)方法
set(int index, E element)方法的作用是指定下標索引處的元素的值。在ArrayList的源碼實現中,方法內首先判斷傳遞的元素數組下標參數是否合法,然後將原來的值取出,設置爲新的值,將舊值作爲返回值返回。

[Java] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
public E set(int index, E element) {
    // 檢驗索引是否合法
    rangeCheck(index);
    // 舊值
    E oldValue = elementData(index);
    // 賦新值
    elementData[index] = element;
    // 返回舊值
    return oldValue;
}




九、ArrayList的indexOf(Object o)方法
indexOf(Object o)方法的作用是從頭開始查找與指定元素相等的元素,如果找到,則返回找到的元素在元素數組中的下標,如果沒有找到返回-1。與該方法類似的是lastIndexOf(Object o)方法,該方法的作用是從尾部開始查找與指定元素相等的元素。

查看該方法的源碼可知,該方法從需要查找的元素是否爲空的角度分爲兩種情況分別討論。這也意味着該方法的參數可以是null元素,也意味着ArrayList集合中能夠保存null元素。方法實現的邏輯也比較簡單,直接循環遍歷元素數組,通過equals方法來判斷對象是否相同,相同就返回下標,找不到就返回-1。這也解釋了爲什麼要把情況分爲需要查找的對象是否爲空兩種情況討論,不然的話空對象調用equals方法則會產生空指針異常。
 

[Java] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
// 從首開始查找數組裏面是否存在指定元素
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;
}




十、ArrayList的get(int index)方法
get(int index)方法是返回指定下標處的元素的值。get函數會檢查索引值是否合法(只檢查是否大於size,而沒有檢查是否小於0)。如果所引致合法,則調用elementData(int index)方法獲取值。在elementData(int index)方法中返回元素數組中指定下標的元素,並且對其進行了向下轉型。

[Java] 純文本查看 複製代碼
1
2
3
4
5
6
public E get(int index) {
    // 檢驗索引是否合法
    rangeCheck(index);
  
    return elementData(index);
}


 

[Java] 純文本查看 複製代碼
1
2
3
E elementData(int index) {
    return (E) elementData[index];
}



十一、ArrayList的remove(int index)方法remove(int index)方法的作用是刪除指定下標的元素。在該方法的源碼中,將指定下標後面一位到數組末尾的全部元素向前移動一個單位,並且把數組最後一個元素設置爲null,這樣方便之後將整個數組不再使用時,會被GC,可以作爲小技巧。而需要移動的元素個數爲:size-index-1。

[Java] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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);
    // 賦值爲空,有利於進行GC
    elementData[--size] = null;
    // 返回舊值
    return oldValue;
}



十二、ArrayList的優缺點
    ArrayList的優點
        ArrayList底層以數組實現,是一種隨機訪問模式,再加上它實現了RandomAccess接口,因此查找也就是get的時候非常快。
        ArrayList在順序添加一個元素的時候非常方便,只是往數組裏面添加了一個元素而已。
        根據下標遍歷元素,效率高。
        根據下標訪問元素,效率高。
        可以自動擴容,默認爲每次擴容爲原來的1.5倍。
    ArrayList的缺點
        插入和刪除元素的效率不高。
        根據元素下標查找元素需要遍歷整個元素數組,效率不高。
        線程不安全。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章