1. 程式人生 > 其它 >Java學習筆記 -- ArrayList集合分析

Java學習筆記 -- ArrayList集合分析

1.ArrayList簡介

ArrayList底層是用陣列實現的,並且它是動態陣列,也就是它的容量是可以自動增長的。

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
  // 略...  
}
  • 實現RandomAccess介面:所以ArrayList支援快速隨機訪問,本質上是通過下標序號隨機訪問。

  • 實現Serializable介面:使ArrayList支援序列化,通過序列化傳輸。

  • 實現Cloneable介面:使ArrayList能夠克隆。

1.1.底層關鍵

ArrayList底層本質上是一個數組,用該陣列來儲存資料:

transient Object[] elementData;

transient:Java關鍵字,變數修飾符,如果用transient宣告一個例項變數,當物件儲存時,它的值不需要維持。換句話來說就是,用transient關鍵字標記的成員變數不參與序列化過程。

1.2.追蹤原始碼

1.2.1.重要屬性

// 預設容量
private static final int DEFAULT_CAPACITY = 10;
// 返回值Object型別的空陣列
private static final Object[] EMPTY_ELEMENTDATA = {};
// 預設容量空陣列
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 儲存我們新增的資料,關鍵
transient Object[] elementData;
// 集合資料長度,因為沒有給預設值所以就是0
private int size;

1.2.2.無參構造

1、在main函式例項化集合物件,並新增元素

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    // 1.首先新增10個元素
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
    // 2.再次新增5個元素
    for (int i = 10; i <= 15; i++) {
        list.add(i);
    }
    list.add(100);
    list.add(200);
}

2、在第一行設斷點,進行Debug,進入到該類的無參建構函式:

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

可以看到,在建構函式裡給存放資料的陣列初始化容量大小為空,因為DEFAULTCAPACITY_EMPTY_ELEMENTDATA就是空陣列。

所以這個時候 list = [ ]

4、第一次走到list.add(i)進入到原始碼裡,如下:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

可以看到,add()內部首先呼叫ensureCapacityInternal()方法,將集合資料長度先加一,再傳入,也就是說第一次傳入的是值是1,因為原先預設為0。該方法主要確定陣列容量是否足夠,是否需要擴容,並不涉及元素的新增,非常重要。

5、進入到該方法當中:

// minCapacity最小容量,首次為 size + 1 = 1
private void ensureCapacityInternal(int minCapacity) { 
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

可以看到,又呼叫了方法的過載,並且首先呼叫calculateCapacity()方法,並且將存放資料的陣列和新增資料到當前時刻的大小傳入,比如第一次新增資料,minCapatiry的大小等於1,第二次就等於2,然後呼叫過載方法,將calculateCapacity()計算的返回結果傳入。

6、首先進入到calculateCapacity()

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
  • 第一次進入到這裡邊,elementData是空的,minCapacity = 1,而判斷語句就是判斷陣列是否為空,所以第一次進入到該方法時,這裡的判斷條件是成立的。

  • 從預設容量DEFAULT_CAPACITY和當前資料大小minCapacity當中取一個最大值返回。因為第一次進入,DEFAULT_CAPACITY = 10minCapacity是傳入的size + 1 = 1

還要一點需要注意的是:如果這裡的minCapacity只在集合新增時使用,那直接返回DEFAULT_CAPACITY即可,因為首次新增資料時minCapacity為1,而DEFAULT_CAPACITY為10,一定大於minCapacity,而下次新增資料陣列就不為空所以往後該判斷均不成立,所以minCapacity不可能會比DEFAULT_CAPACITY 大,之所以還要呼叫方法挑選最大的返回,是因為ArrayList支援序列化和反序列化,如果直接通過反序列化獲得一個集合扔過來,那麼minCapacity的值可能是非常大的,這個時候是需要基於當前大小進行再次擴容。

所以、第一次最終的返回結果就是 10,這個10代表我們需要的陣列容量大小,也就是說、首次新增資料發現數組為空,就直接擴容到10的大小。

6、進入過載方法:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
  • 首先是modCount++,這是記錄集合被修改的次數,因為ArrayList是非執行緒安全的。

  • 接著判斷我們需要的最小容量減去當前資料長度是否大於0,如果條件成立,說明實際需要的容量大小已經大於原始陣列容量的大小了,所以就呼叫grow()進行擴容,第一次新增資料,calculateCapacity()返回的結果是return Math.max(DEFAULT_CAPACITY, minCapacity),也就是10,說明我們實際需要的容量為10才夠用,但是陣列卻是空的,所以條件必定成立,然後進行擴容,如下:

7、進入到grow()方法當中:

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. 首先獲取陣列長度,賦值給oldCapacity,也就是0,這是原始容量大小。
  2. 然後進行位運算,向右移一位,也就是原始資料除以2,然後加上oldCapacity,由於是第一次,所以0 + 0 = 0,這是進行位運算後或者是擴容後的新容量大小。
  3. 這一步非常關鍵,判斷擴容後的容量減去最小容量是否小於0,如果條件成立,就將執行newCapacity = minCapacity。也就是執行了newCapacity = 10,最後將新的容量複製給原始陣列,也就是將一個空陣列擴容到了大小為 10 的陣列。

所以得出結論:初始化ArrayList陣列預設為空,新增第一個資料之前將陣列大小擴容到10,往後如果超出10,就會基於10進行1.5倍擴容,這裡設計的非常繞,繞來繞去的就是為了擴容到10。

1.2.3.有參構造

如果使用的是有參構造,則初始化容量為指定大小,如下:

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

新增資料呼叫add()方法,進入calculateCapacity()方法,如下:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

由於例項化的是有參構造,在裡邊指定了陣列大小,這時判斷就不成立了,因為陣列不為空,而第一次新增資料minCapacity依舊為1。

接著進入ensureExplicitCapacity()

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

下方的條件依舊不成立,第一次新增資料minCapacity等於1,而陣列大小可能為我們指定的8,所以不需要擴容。

所以:如果使用有參構造例項化集合物件,則初始化elementData為指定大小,如果需要擴容,則直接擴容elementData的1.5倍。