淺談 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(容量等於0的話,只剩這一種情況了)
    } else { return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } }

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

  如果當前容量大於0,或者當前數組不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,前面說明過,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一個被final修飾的空數組,在三個構造方法中,只有無參構造方法中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。那么什么時候是這種情況呢?

    (1)傳容量的構造方法傳入的是0時,elementData被賦予的是EMPTY_ELEMENTDATA,此時數組容量為0,添加元素時,符合if的條件,會進入此擴容情況,容量從0擴展到1。

    (2)傳Collection元素列表的構造方法被傳入空列表時,elementData被賦予的是EMPTY_ELEMENTDATA,數組容量為0,此時添加元素時,符合if的條件,會進入此擴容情況,容量從0擴展到1。

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

  else就剩一種情況了,也就是用默認無參構造方法創建的數組的初始擴容情況。此時的容量為0,添加一個元素時會創建一個新的數組,其大小為max(DEFAULT_CAPACITY, minCapacity)。

  我們從上面的源碼變量信息中可得知DEFAULT_CAPACITY是一個常量,其值為10,而minCapacity的值為(0+1),所以添加一個元素時,max(DEFAULT_CAPACITY, minCapacity)的值必為10。也就是說,當我們用默認無參構造方法創建的數組在添加元素前,ArrayList的容量為0,添加一個元素后,ArrayList的容量就變為10了。

總結一下

ArrayList的特點:

  1.ArrayList的底層數據結構是數組,所以查找遍歷快,增刪慢。

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

  3.ArrayList的線程是不安全的。

ArrayList的擴容:

  擴容可分為兩種情況:

  第一種情況,當ArrayList的容量為0時,此時添加元素的話,需要擴容,三種構造方法創建的ArrayList在擴容時略有不同:

    1.無參構造,創建ArrayList后容量為0,添加第一個元素后,容量變為10,此后若需要擴容,則正常擴容。

    2.傳容量構造,當參數為0時,創建ArrayList后容量為0,添加第一個元素后,容量為1,此時ArrayList是滿的,下次添加元素時需正常擴容。

    3.傳列表構造,當列表為空時,創建ArrayList后容量為0,添加第一個元素后,容量為1,此時ArrayList是滿的,下次添加元素時需正常擴容。

  第二種情況,當ArrayList的容量大於0,並且ArrayList是滿的時,此時添加元素的話,進行正常擴容,每次擴容到原來的1.5倍。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM