【Java集合】ArrayList原始碼分析

就行222發表於2021-07-12

ArrayList是日常開發中經常使用到的集合,其底層採用陣列實現,因此元素按序存放。其優點是可以使用下標來訪問元素,時間複雜度是O(1)。其缺點是刪除和增加操作需要使用System.arraycopy()來移動部分受影響的元素,時間複雜度為O(N)。同時ArrayList由於是採用陣列來存放資料,因此會有容量限制,在使用時需要擴容,當插入操作超出陣列長度,就會進行擴容,擴容後陣列的長度為原來的1.5倍,預設的陣列長度是10。

為了更好的掌握ArrayList,因此閱讀並仿照ArrayList原始碼,實現一個具有增刪改查以及自動擴容功能的簡易版MyArrayList(程式碼幾乎與ArrayList原始碼一致)。

首先新建一個class,命名為MyArrayList

public class MyArrayList<E> {
}

由於ArrayList是通過陣列來儲存元素的,因此我們定義一個Object型別的陣列elementData來儲存資料,再定義一個變數size,用來記錄陣列中的元素個數,其預設值為0。

public class MyArrayList<E> {
	private Object[] elementData; //ArrayList儲存元素的物件陣列
	private int size; //ArrayList儲存元素的個數
	
}

接下來就是建構函式,有兩種,第一種是未指定初始容量的建構函式,預設容量為10;第二種是在建構函式中傳入指定容量。

先說第一種建構函式,ArrayList在預設情況下,其容量為10。因此我們定義一個常量DEFAULT_CAPACITY = 10作為預設容量。同時,還定義一個常量陣列DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}用於對elementData進行初始賦值。

public class MyArrayList<E> {
	private Object[] elementData; //ArrayList儲存元素的物件陣列
	private int size; //ArrayList儲存元素的個數

	private final static int DEFAULT_CAPACITY = 10; //ArrayList的物件陣列的預設初始容量
	private final static Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //ArrayList物件陣列的預設初始化

	/**
     * 不指定初始容量的建構函式
     */
    public MyArrayList(){
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
}

需要注意的是這裡的預設容量10並不是在建構函式中直接使用,而是在第一次插入進行擴容時才會使用。

第二種建構函式,傳入指定的容量。根據傳入的容量引數,我們有以下三種結果:
①傳入的容量引數大於0:則以該引數為容量建立物件陣列
②存入的容量引數等於0:則需要建立一個空的物件陣列,因此定義一個常量陣列EMPTY_ELEMENTDATA = {}用於傳入容量為0時的初始化。
③傳入的容量引數小於0:明顯這是非法的,因此丟擲引數異常IllegalArgumentException()

public class MyArrayList<E> {
	private Object[] elementData; //ArrayList儲存元素的物件陣列
	private int size; //ArrayList儲存元素的個數

	private final static int DEFAULT_CAPACITY = 10; //ArrayList的物件陣列的預設初始容量
	private final static Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //ArrayList物件陣列的預設初始化
	private static final Object[] EMPTY_ELEMENTDATA = {}; //傳入容量為0時的初始化

	/**
     * 不指定初始容量的建構函式
     */
    public MyArrayList(){
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    /**
     * 傳入指定初始容量的建構函式
     * @param initialCapacity 傳入的初始化容量
     */
    public MyArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("非法的容量: "+
                    initialCapacity);
        }
    }
}

好了,建構函式構建完畢,接下來就是增刪改查功能的實現,實現的方法如下:

//增,將元素放到陣列末尾元素的後面,e為待插入元素,返回boolean
boolean add(E e)

//刪,刪除指定位置的元素,index為待刪除元素的位置,返回被刪除的元素
E remove(int index)

//改,替換指定位置的元素,index為被替換元素的位置,e為替換的元素,返回被替換的元素
E set(int index, E e)

//查,查詢指定位置的元素,index為查詢的位置,返回查到的元素
E get(int index)

首先是add(E e)方法,由於陣列容量有限制,因此我們新增一個元素,都有可能要進行擴容,所以我們需要編寫一個函式ensureCapacityInternal來判斷是否需要自動擴容,若需要則進行擴容。

 	/**
     * ArrayList的add方法
     * 將元素放到陣列末尾元素的後面
     * @param e 待插入的元素
     * @return 
     */
    boolean add(E e){
        //1、自動擴容機制,傳入的是目前需要的最小容量
        ensureCapacityInternal(size + 1);
    }

在ensureCapacityInternal函式中,需要傳入目前需要的最小容量。同時我們還要判斷物件陣列elementData是否為空陣列,若是,則將傳入的目前需要的最小容量與預設容量10進行對比,取其中的最大值作為本次擴容的容量。

/**
     * 判斷原陣列是否為空陣列
     * 是:則選預設容量和目前需要的最小容量二者中的最小值
     * 否:則繼續往下判斷
     * @param minCapacity 目前需要的最小容量
     */
    void ensureCapacityInternal(int minCapacity){
        // elementData 為空陣列,則將傳入的minCapacity與預設的初始容量DEFAULT_CAPACITY進行對比,取兩者中最大值
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA){
            minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
        }

        //接著往下判斷
        ensureExplicitCapacity(minCapacity);
    }

接下來,我們判斷是否需要進行擴容。如果目前需要的最小容量大於原陣列的長度,才進行擴容,否則不進行擴容,該功能寫入函式ensureExplicitCapacity。

    /**
     * 判斷是否需要進行擴容
     * 如果目前需要的最小長度大於原陣列的長度,才進行擴容
     * 否則不進行擴容
     * @param minCapacity  目前需要的最小容量
     */
    void ensureExplicitCapacity(int minCapacity){
        //目前需要的最小容量超過原陣列長度,才進行擴容,否則就不擴容
        if (minCapacity - elementData.length > 0) {
            grow(minCapacity); //擴容
        }
    }

然後,若進行擴容,則執行擴容函式grow。在grow中,我們需要進行如下操作:
①獲取原陣列的容量oldCapacity,然後計算出值為oldCapacity1.5倍的新容量newCapacity

int oldCapacity = elementData.length;
//擴容為原來的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);

②將擴容1.倍後的新容量newCapacity與目前需要的最小容量minCapacity進行對比,若新容量小於目前需要的最小容量,則新容量的值取目前需要的最小容量。

if (newCapacity - minCapacity < 0) newCapacity = minCapacity;

③將新容量newCapacity與所允許的陣列的最大長度進行對比,陣列最大長度定義為常量MAX_ARRAY_SIZE = Integer.MAX_VALUE,值為整數的最大值。如果新容量newCapacity大於陣列最大長度MAX_ARRAY_SIZE ,則取目前需要的最小容量minCapacity與陣列最大長度MAX_ARRAY_SIZE兩者中的最小值作為新容量newCapacity的值。

 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = (minCapacity - MAX_ARRAY_SIZE) > 0 ? MAX_ARRAY_SIZE : minCapacity;

④使用Arrays.copyOf(原陣列, 新長度)進行陣列的複製,即實現陣列擴容

elementData = Arrays.copyOf(elementData,newCapacity);

完成擴容任務的函式grow如下:

 /**
     * 擴容函式:如何進行擴容(擴多少)
     * ①擴容1.5倍
     * ②若擴容1.5倍還不滿足需要的最小容量,則擴容長度為目前需要的最小容量
     * ③若新的容量大於陣列所允許的最大長度,則取需要的最小容量與陣列所允許的最大長度
     * 兩者中的最小值
     * @param minCapacity 目前需要的最小容量
     */
    void grow(int minCapacity){
        int oldCapacity = elementData.length;
        //oldCapacity原陣列長右移1位,即相當於除2,最後加原長度,則為擴容1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果擴容1.5倍後的新容量小於需要的最小容量,則新的容量即為傳入的最小容量
        if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
        //如果新容量大於陣列能夠允許的最大長度,則看傳入的最小容量與陣列最大長度對比,取其中的小者
        if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = (minCapacity - MAX_ARRAY_SIZE) > 0 ? MAX_ARRAY_SIZE : minCapacity;

        //Arrays.copyOf(原陣列, 新長度),返回新陣列。使用該函式完成自動擴容
        elementData = Arrays.copyOf(elementData,newCapacity);
    }

至此,就完成了新增時,判斷是否需要擴容,並且完成擴容功能。接下來我們只需要將新增元素插入陣列元素末尾位置的下一個位置,並返回true即可。

boolean add(E e){
        //1、自動擴容機制,傳入的是目前需要的最小容量
        ensureCapacityInternal(size + 1);

        //2、擴容完畢,將元素存入
        elementData[size++] = e;

        return true;
}

最終,新增add方法和自動擴容有關的函式編寫完成:

	/**
     * ArrayList的add方法
     * 將元素放到陣列末尾元素的後面
     * @param e 待插入的元素
     * @return 
     */
boolean add(E e){
        //1、自動擴容機制,傳入的是目前需要的最小容量
        ensureCapacityInternal(size + 1);

        //2、擴容完畢,將元素存入
        elementData[size++] = e;

        return true;
}

	/**
     * 判斷原陣列是否為空陣列
     * 是:則選預設容量和目前需要的最小容量二者中的最小值,然後接著往下判斷
     * 否:則直接繼續往下判斷
     * @param minCapacity 目前需要的最小容量
     */
    void ensureCapacityInternal(int minCapacity){
        // elementData 為空陣列,則將傳入的minCapacity與預設的初始容量DEFAULT_CAPACITY進行對比,取兩者中最大值
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA){
            minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
        }

        //接著往下判斷
        ensureExplicitCapacity(minCapacity);
    }

 	/**
     * 判斷是否需要進行擴容
     * 如果目前需要的最小長度大於原陣列的長度,才進行擴容
     * 否則不進行擴容
     * @param minCapacity  目前需要的最小容量
     */
    void ensureExplicitCapacity(int minCapacity){
        //目前需要的最小容量超過原陣列長度,才進行擴容,否則就不擴容
        if (minCapacity - elementData.length > 0) {
            grow(minCapacity); //擴容
        }
    }

	/**
     * 擴容函式:如何進行擴容(擴多少)
     * ①擴容1.5倍
     * ②若擴容1.5倍還不滿足需要的最小容量,則擴容長度為目前需要的最小容量
     * ③若新的容量大於陣列所允許的最大長度,則取需要的最小容量與陣列所允許的最大長度
     * 兩者中的最小值
     * @param minCapacity 目前需要的最小容量
     */
    void grow(int minCapacity){
        int oldCapacity = elementData.length;
        //oldCapacity原陣列長右移1位,即相當於除2,最後加原長度,則為擴容1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果擴容1.5倍後的新容量小於需要的最小容量,則新的容量即為傳入的最小容量
        if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
        //如果新容量大於陣列能夠允許的最大長度,則看傳入的最小容量與陣列最大長度對比,取其中的小者
        if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = (minCapacity - MAX_ARRAY_SIZE) > 0 ? MAX_ARRAY_SIZE : minCapacity;

        //Arrays.copyOf(原陣列, 新長度),返回新陣列。使用該函式完成自動擴容
        elementData = Arrays.copyOf(elementData,newCapacity);
    }

接下來,就是刪除,remove方法。由於該方法傳入待刪除元素的位置索引index,因此需要檢查index
的範圍是否符合要求。編寫一個函式rangeCheck來檢查下標。

	/**
     * 檢查index範圍
     * 超出範圍則丟擲異常
     * @param index 陣列下標位置
     */
     void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException("哎呀,超出範圍了!");
    }

若index沒有超出範圍,則接下來就是獲取索引對應的元素,獲取方式很簡單,就是elementData[index]即可。考慮到其他方法也會需要通過這樣方式來獲取對應位置的元素,因此我們將這個操作抽取出來,成為一個函式elementData(),用於獲取元素。

 	/**
     * 返回陣列中指定位置的元素
     * @param index
     * @return
     */
    E elementData(int index){
        return (E) elementData[index];
    }

那麼,目前remove方法前面兩個操作我們已經完成

    E remove(int index){
        //1、檢查index範圍
        rangeCheck(index);
        //2、獲取index對應的元素
        E oldValue = elementData(index);
       
    }

刪除index元素,需要把該位置後面的所有元素都向前移動一個位置。因此接下來我們就需要將index後面的元素向前移動一個位置。

具體做法是,先計算出需要移動的元素個數numMoved,用陣列中最後一個元素的下標減去index即可獲得需要移動的元素個數:size-1-index。

然後利用System.arraycopy()來移動元素,該方法的用法如下:

System.arrayCopy(Object srcArray,int srcPos,Object destArray ,int destPos,int length)
①Object srcArray 原陣列(要拷貝的陣列)
②int srcPos 要複製的原陣列的起始位置(陣列從0位置開始)
③ Object destArray 目標陣列
④ int destPos 目標陣列的起始位置
⑤int length 要複製的長度
從原陣列srcArray 取元素,範圍為下標srcPos到srcPos+length-1,取出共length個元素,存放到目標陣列中,存放位置為下標destPos到destPos+length-1。

我們將原陣列和目標陣列都設為elementData,然後原陣列的起始位置為index+1,目標陣列的起始位置為index,要複製的長度設為元素個數numMoved。這樣就能做到將陣列index位置後面的元素向前移動一位。

不過這樣做目標陣列的最後一位元素依然是原來的數,因此我們需要將目標陣列最後的元素置為null,並且由於是刪除,所以元素個數size需要減一。至此,刪除方法remove完成。

	/**
     * ArrayList的remove方法
     * @param index 要刪除元素的位置
     * @return 返回被刪除元素
     */
    E remove(int index){
        //1、檢查index範圍
        rangeCheck(index);
        //2、獲取index對應的元素
        E oldValue = elementData(index);
        //3、計算需要移動的元素的個數
        int numMoved = size - 1 - index;
        //4、將index後面的數往前移動一位
        if (numMoved > 0){
            System.arraycopy(elementData,index + 1, elementData, index, numMoved);
        }
        //5、把最後的元素置為null
        elementData[--size] = null;
        //返回被刪除元素
        return oldValue;
    }

增刪操作已完成,接下來就是改操作,set()方法。這個方法就比較簡單,具體的步驟如下:
①檢查index範圍
②獲取index位置的元素
③將index位置的元素,替換為傳入的元素
④返回原先index位置的元素

	/**
     * ArrayList的set
     * @param index 需要修改的位置
     * @param e 需要替換的元素
     */
    E set(int index, E e){
        //1、檢查index範圍
        rangeCheck(index);

        //2、獲取指定位置的元素
        E oldValue = elementData(index);

        //3、替換元素
        elementData[index] = e;

        //4、返回原先index位置的元素
        return oldValue;
    }

最後,就是查操作,get方法。該方法更為簡單,只需要先檢查index範圍,再獲取index位置的元素直接返回即可。

	/**
     * ArrayList的get方法
     * @param index
     * @return
     */
    E get(int index){
        //1、檢查index範圍
        rangeCheck(index);

        //2、獲取指定位置的元素
        return elementData(index);
    }

到這裡,我們編寫的簡易版ArrayList的增刪改查操作就全部完成了。點進JDK1.8中ArrayList原始碼可以看到,我們的上面的程式碼幾乎與ArrayList原始碼一模一樣。

最終這個簡易版ArrayList所有程式碼如下:

public class MyArrayList<E> {

    private int size; //ArrayList中實際元素的數量的size
    private Object[] elementData; //ArrayList的物件陣列

    private final static int DEFAULT_CAPACITY = 10; //ArrayList的物件陣列的預設初始容量
    private final static int MAX_ARRAY_SIZE = Integer.MAX_VALUE; //陣列的最大長度,也就是整數的最大值
    private final static Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //ArrayList的物件陣列的預設初始化
    private static final Object[] EMPTY_ELEMENTDATA = {}; //傳入容量為0時的初始化

    /**
     * 不指定初始容量的建構函式
     */
    public MyArrayList(){
        elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 傳入指定初始容量的建構函式
     * @param initialCapacity
     */
    public MyArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("非法的容量: "+
                    initialCapacity);
        }
    }

    /**
     * ArrayList的add方法
     * 將元素放到陣列有效長度的末尾
     * @param e 待插入的元素
     * @return
     */
    boolean add(E e){
        //1、自動擴容機制,傳入的是目前需要的最小容量
        ensureCapacityInternal(size + 1);

        //2、擴容完畢,將元素存入
        elementData[size++] = e;

        return true;
    }


    /**
     * 判斷原陣列是否為空陣列
     * 是:則選預設容量和目前需要的最小容量二者中的最小值,然後接著往下判斷
     * 否:則直接繼續往下判斷
     * @param minCapacity 目前需要的最小容量
     */
    void ensureCapacityInternal(int minCapacity){
        // elementData 為空陣列,則將傳入的minCapacity與預設的初始容量DEFAULT_CAPACITY進行對比,取兩者中最大值
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA){
            minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
        }

        //接著往下判斷
        ensureExplicitCapacity(minCapacity);
    }

    /**
     * 判斷是否需要進行擴容
     * 如果目前需要的最小長度大於原陣列的長度,才進行擴容
     * 否則不進行擴容
     * @param minCapacity  目前需要的最小容量
     */
    void ensureExplicitCapacity(int minCapacity){
        //目前需要的最小容量超過原陣列長度,才進行擴容,否則就不擴容
        if (minCapacity - elementData.length > 0) {
            grow(minCapacity); //擴容
        }
    }

    /**
     * 擴容函式:如何進行擴容(擴多少)
     * ①擴容1.5倍
     * ②若擴容1.5倍還不滿足需要的最小容量,則擴容長度為目前需要的最小容量
     * ③若新的容量大於陣列所允許的最大長度,則取需要的最小容量與陣列所允許的最大長度
     * 兩者中的最小值
     * @param minCapacity 目前需要的最小容量
     */
    void grow(int minCapacity){
        int oldCapacity = elementData.length;
        //oldCapacity原陣列長右移1位,即相當於除2,最後加原長度,則為擴容1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果擴容1.5倍後的新容量小於需要的最小容量,則新的容量即為傳入的最小容量
        if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
        //如果新容量大於陣列能夠允許的最大長度,則看傳入的最小容量與陣列最大長度對比,取其中的小者
        if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = (minCapacity - MAX_ARRAY_SIZE) > 0 ? MAX_ARRAY_SIZE : minCapacity;

        //Arrays.copyOf(原陣列, 新長度),返回新陣列。使用該函式完成自動擴容
        elementData = Arrays.copyOf(elementData,newCapacity);
    }

    /**
     * ArrayList的remove方法
     * @param index 要刪除元素的位置
     * @return 返回被刪除元素
     */
    E remove(int index){
        //1、檢查index範圍
        rangeCheck(index);
        //2、獲取index對應的元素
        E oldValue = elementData(index);
        //3、計算需要移動的元素的個數
        int numMoved = size - 1 - index;
        //4、將index後面的數往前移動
        if (numMoved > 0){
            System.arraycopy(elementData,index + 1, elementData, index, numMoved);
        }
        //5、把最後的元素置為null
        elementData[--size] = null;
        //返回被刪除元素
        return oldValue;
    }

    /**
     * 檢查index範圍
     * 超出範圍則報錯
     * @param index
     */
    void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException("哎呀,超出範圍了!");
    }

    /**
     * 返回陣列中指定位置的元素
     * @param index
     * @return
     */
    E elementData(int index){
        return (E) elementData[index];
    }


    /**
     * ArrayList的set
     * @param index 需要修改的位置
     * @param e 需要替換的元素
     */
    E set(int index, E e){
        //1、檢查index範圍
        rangeCheck(index);

        //2、獲取指定位置的元素
        E oldValue = elementData(index);

        //3、替換元素
        elementData[index] = e;

        //4、返回原先index位置的元素
        return oldValue;
    }

    /**
     * ArrayList的get方法
     * @param index
     * @return
     */
    E get(int index){
        //1、檢查index範圍
        rangeCheck(index);

        //2、獲取指定位置的元素
        return elementData(index);
    }


    /**
     * 獲取元素個數
     * @return
     */
    int size(){
        return size;
    }

}

我們測試一下,這個簡易版ArrayList

 public static void main(String[] args) {
        MyArrayList<String> myArrayList = new MyArrayList<>();
        //增
        myArrayList.add("hello");
        myArrayList.add("word");
        myArrayList.add("hello");
        myArrayList.add("java");
        //改
        myArrayList.set(1,"####");
        //刪
        myArrayList.remove(2);
        //查
        for (int i = 0; i < myArrayList.size(); i++){
            System.out.println(myArrayList.get(i));
        }

    }

測試結果如下:

hello
####
java

相關文章