1. 程式人生 > 實用技巧 >淺談 ArrayList 及其擴容機制

淺談 ArrayList 及其擴容機制

淺談ArrayList

  ArrayList類又稱動態陣列,同時實現了Collection和List介面,其內部資料結構由陣列實現,因此可對容器內元素實現快速隨機訪問。但因為ArrayList中插入或刪除一個元素需要移動其他元素,所以不適合在插入和刪除操作頻繁的場景下使用。

  ArrayList的容量可以隨著元素的增加而自動增加,因此不用擔心ArrayList容量不足的問題。

  ArrayList是非執行緒安全的

  接下來,我們將解析ArrayList的構造方法,在看構造方法之前,我們先來明確一下ArrayList原始碼中的一些概念。這些變數和物件大家先記住就好了,後面會看到它們的用途。

// 預設的容量大小(常量)
private static final int DEFAULT_CAPACITY = 10;

// 定義的空陣列(final修飾,大小固定為0)
private static final Object[] EMPTY_ELEMENTDATA = {};

// 定義的預設空容量的陣列(final修飾,大小固定為0)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 定義的不可被序列化的陣列,實際儲存元素的陣列
transient Object[] elementData; 

// 陣列中元素的個數
private int size;

  ArrayList有三種構造方法:

    1.無參的構造方法

    2.根據傳入的數值大小,建立指定長度的陣列

    3.通過傳入Collection元素列表進行生成

1.無參的構造方法

// 無參的構造方法
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

  可以看出來,當我們直接建立ArrayList時,elementData被賦予了預設空容量的陣列。注意,因為預設空容量陣列是被final修飾的,此時ArrayList陣列是空的、固定長度的

,也就是說其容量此時是0,元素個數size為預設值0。

2.根據傳入的數值大小,建立指定長度的陣列

// 傳容量的構造方法
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);
    }
}

  當initialCapacity > 0時,會在堆上new一個大小為initialCapacity的陣列,然後將其引用賦給elementData,此時ArrayList的容量為initialCapacity,元素個數size為預設值0。

  當initialCapacity = 0時,elementData被賦予了預設空陣列,因為其被final修飾了,所以此時ArrayList的容量為0,元素個數size為預設值0。

  當initialCapacity < 0時,會丟擲異常。

3.通過傳入Collection元素列表進行生成

// 傳入Collection元素列表的構造方法
public ArrayList(Collection<? extends E> c) {
    // 將列表轉化為對應的陣列
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // 此處見下面詳細解析
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 賦予空陣列
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

  傳入Collection元素列表後,構造方法首先會將其轉化為陣列,將其索引賦給elementData。如果此陣列的長度為0,會重新賦予elementData為空陣列,此時ArrayList的容量是0,元素個數size為0。

  如果陣列的長度不為0,會跟新size的大小為其長度,也就是元素的個數,然後執行裡面的程式。大家對裡面的程式碼可能不理解,讓我們等會看下面解析。執行完後此時ArrayList的容量為傳入序列的長度,也就是size的大小,同時元素個數也為size,也就是說,此時ArrayList是滿的。

  讓我們來看看下面的程式碼,然後再去理解上面 if 語句的程式碼:

public class Test {
    public static void main(String[] args) {
        // 1.建立Student物件陣列
        Student[] students = new Student[] {
                new Student("小明", 18),
                new Student("小李", 19),
                new Student("小張", 21)
        };
        
        // 2.將其賦值給Object物件陣列
        Object[] objects = students;
        
        // 3.執行if語句前,列印陣列的class
        System.out.println("執行前:" + objects.getClass());
        
        // 4.執行上面的程式碼
        if (objects.getClass() != Object[].class) {
            objects = Arrays.copyOf(objects, objects.length, Object[].class);
        }
        
        // 5.執行if語句後,列印陣列的class
        System.out.println("執行後:" + objects.getClass());
    }
}

  程式的執行結果如下:

  可以看到,物件陣列也是有.class的,其中含有所儲存元素的型別,而上面的那段程式碼的作用就是將原物件陣列的陣列型別轉化為Object物件陣列的陣列型別,以便更好的儲存。

ArrayList的擴容機制

  當我們探討擴容時,肯定要從ArrayList的add方法走起,讓我們來看看吧。

public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}

  這是最基本的add方法,當然,也是可以來說明問題的。可以看到,此add方法的引數就是一個被加元素,moCount是記錄ArrayList被修改的次數的,可以不用管。然後是另一個add方法,所傳的值是被加元素、當前陣列和當前陣列的元素個數,讓我們來看看這個add方法吧。

private void add(E e, Object[] elementData, int s) {
    // 判斷元素個數是否等於當前容量
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}

  首先,它判斷了元素個數是否等於當前的容量,也就是判斷當前陣列空間是不是滿的,如果二者相等,則當前空間是滿的就需要擴容了,grow函式就是擴容函式了,擴容後再將被加元素加到陣列中

  下面我們來看看grow函式是什麼樣子的:

private Object[] grow() {
    return grow(size + 1);
}

  裡面呼叫了一個帶參的grow函式引數是當前元素個數+1,也就是當前容量+1返回的是這個函式的返回值,讓我們進一步研究。

private Object[] grow(int minCapacity) {
    // 獲取老容量,也就是當前容量
    int oldCapacity = elementData.length;
    // 如果當前容量大於0 或者 陣列不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    // 如果是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,建立新陣列
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

  首先,它是記錄了一下老容量的大小,然後再進行下面的操作。

  如果當前容量大於0,或者當前陣列不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,前面說明過,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一個空的長度固定為0的陣列,在三個構造方法中,只有無參構造方法中elementData被賦予了DEFAULTCAPACITY_EMPTY_ELEMENTDATA。也就是說,這個if語句中不會處理用預設無參構造方法建立的陣列的初始擴容情況,那麼其餘的情況都是由此if語句來處理的

  我們來看一下if裡面的操作先建立一個新的陣列,然後將舊陣列拷貝到新陣列並賦給elementData返回。ArraysSupport.newLength函式的作用是建立一個大小為oldCapacity+max(minimum growth, preferred growth)的陣列。minCapacity是傳入的引數,我們上面看過,它的值是當前容量(老容量)+1,那麼minCapacity - oldCapacity的值恆為1,minimum growth的值恆為1;oldCapacity >> 1的功能是將oldCapacity 進行位操作,右移一位,也就是減半,preferred growth的值為oldCapacity 的一半。

  當oldCapacity 為0時,右移後還是0,也就是說此時擴容的大小為0+max(1,0)=1,容量從0擴充套件到1那麼什麼時候是這種情況呢?當傳容量的構造方法傳入的是0時,elementData被賦予的是EMPTY_ELEMENTDATA,陣列容量為0,此時新增元素時,符合if的條件,會進入此擴容情況,容量從0擴充套件到1。當傳Collection元素列表的構造方法被傳入空列表時,elementData被賦予的是EMPTY_ELEMENTDATA,陣列容量為0,此時新增元素時,符合if的條件,會進入此擴容情況,容量從0擴充套件到1。

  當oldCapacity 大於0時,新建立的陣列大小是老容量+老容量的一半,也就是老容量的1.5倍,每次擴容到原來的1.5倍。

  if之外就剩一種情況了,也就是用預設無參構造方法建立的陣列的初始擴容情況。此時的容量為0,新增一個元素時會建立一個新的陣列,其大小為max(DEFAULT_CAPACITY, minCapacity),我們從上面的原始碼變數資訊中可得知DEFAULT_CAPACITY的值為10,而minCapacity的值為1,所以新增一個元素時,max(DEFAULT_CAPACITY, minCapacity)的值必為10。也就是說,當我們用預設無參構造方法建立的陣列在新增元素前,ArrayList的容量為0,新增一個元素後,ArrayList的容量為10。

總結一下

ArrayList的特點:

  1.ArrayList的底層資料結構是陣列,所以查詢遍歷快,增刪慢。

  2.ArrayList可隨著元素的增長而自動擴容,正常擴容的話,每次擴容到原來的1.5倍。

  3.ArrayList的執行緒是不安全的。

ArrayList的擴容:

  在兩種情況下需要擴容:

  第一種情況,當ArrayList的容量為0時,此時新增元素的話,需要擴容。什麼時候容量可能為0呢?當然是剛被建立的時候,三種構造方法建立的ArrayList在擴容時略有不同

    (1)無參構造,建立ArrayList後容量為0,新增第一個元素後,容量變為10,此後若需要擴容,則正常擴容。

    (2)傳容量構造,當引數為0時,建立ArrayList後容量為0,新增第一個元素後,容量為1,此時ArrayList是滿的,下次新增元素時需正常擴容。

    (3)傳列表構造,當列表為空時,建立ArrayList後容量為0,新增第一個元素後,容量為1,此時ArrayList是滿的,下次新增元素時需正常擴容。

  第二種情況,當ArrayList的容量大於0時,此時新增元素若需要擴容的話,正常擴容,每次擴容到原來的1.5倍。