集合框架學習之ArrayList源碼(一)

ArrayList是我們經常使用到的一個集合類,通過查看其底層源碼實現,有利於我們更好的掌握與使用。ArrayList不算多,但是也不算少,挨個看下去也挺費時費力的。所以呢,我打算循序漸進着來,比如這一篇就先看看ArrayList的屬性,以及一些常用的方法,例如增、刪、改、查。一口吃不成胖子,任重而道遠~

1、ArrayList的結構


下面是在IDEA裏查看到的ArrayList的UML圖:

從圖中我們可以看到:

  • 總的來看,ArrayList 繼承了AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable這些接口;
  • ArrayList 實現了RandmoAccess接口,即提供了隨機訪問功能。RandmoAccess是java中用來被List實現,爲List提供快速訪問功能的。這表明在ArrayList中,我們即可以通過元素的序號快速獲取元素對象(因爲底層實現是數組),這就是快速隨機訪問;
  • ArrayList 實現了Cloneable接口,即覆蓋了函數clone(),能被克隆;
  • ArrayList 實現java.io.Serializable接口,這意味着ArrayList支持序列化,能通過序列化去傳輸。

整體架構還是非常清晰的,那麼開始進入ArrayList的源碼開始探索內部原理。

2 、ArrayList的屬性分析


通過IDEA查看ArrayList的結構信息,圖中框出來的部分是它的屬性:

庖丁解牛,挑出重要的來一個個分析:

  • private static final int DEFAULT_CAPACITY = 10:定義了ArrayList對象的默認容量爲10;
  • private static final Object[] EMPTY_ELEMENTDATA = {} :定義了一個空的數組,在new ArrayList對象時如果參數爲0,那麼得到的這個空的數組;
  • private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}:同樣是定義了一個空的數組,不過和上邊的同,在new ArrayList對象時不傳參,那麼得到的這個空的數組;
  • transient Object[] elementData:ArrayList中真正存儲元素的數組;
  • private int size:用來表示數組中元素的個數。

這些字段單獨看來沒什麼意義,在使用過程中再細細體會其意義所在。

3、ArrayList的構造方法


通過IDEA查看類的結構信息,找出其中的構造方法:

可以看到ArrayList的構造方法共有三個重載,分別是:定義初始容量、默認無參構造和傳入一個Collection集合三種構造方法。

那麼接下來對這三個構造方法進行分析:

3.1 定義初始容量的構造方法

這種創建對象的方式很簡單,就是在new對象時給定一個初始值:

    public static void main(String[] args) {

        List list = new ArrayList(50);   //初始容量爲50

    }

那麼它是如何操作創建對象的囁?查看它的具體源代碼:(具體的分析將在註釋中寫出)

   /**
     * Constructs an empty list with the specified initial capacity.
     * 構造具有指定初始容量的空列表。
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {          
            //如果給的初始值>0,創建一個大小爲initialCapacity的Object對象數組
            this.elementData = new Object[initialCapacity]; 
        } else if (initialCapacity == 0) {
            //如果給的初始值=0,則將EMPTY_ELEMENTDATA空數組賦給它
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //如果給的初始值<0,則拋出異常信息
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

3.2 默認的無參構造方法

這個方法就相對簡單了,看源代碼

    /**
     * Constructs an empty list with an initial capacity of ten.
     * 構造一個初始容量爲10的空列表。
     */
    public ArrayList() {
        // 將默認的空的數組賦值給它
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

不是說構造一個初始容量爲10的列表嗎?10呢~?其實是當元素第一次被加入時,擴容至默認容量 10。先留個坑,後邊再說。

3.3 傳入一個Collection集合的構造方法

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     * 構造一個包含指定集合的元素的列表,按集合的迭代器返回元素的順序排列。
     *
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     * 如果傳入的集合爲null,拋出空指針異常。
     */
    public ArrayList(Collection<? extends E> c) {
        
        // 先將傳入的集合轉化爲數組,然後ArrayList底層數組elementData指向該數組
        elementData = c.toArray();  
        //將elementData中的元素個數賦值給sizes
        if ((size = elementData.length) != 0) {
            
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            //保姆級別翻譯:c.toArray方法可能返回的不是一個Object數組,詳見6260652號BUG 
             if (elementData.getClass() != Object[].class)
            //如果c.toArray() 返回的數組類型不是 Object[],則使用Arrays.copyOf()來構             //造一個大小爲 size的Object[]類型的數組給elementData數組
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

有個點我們需要注意一下,我們經常會通過size()方法查看一個集合中元素的個數,但是注意size和elementData.length是不相同的。size是指當前集合中存在的元素個數,elementData.length是指當前集合指定的容量大小。例如,如果在new ArrayList()時,ArrayList只是給我們默認的elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA,此時只是空數組,只有第一次調用add()時,纔將默認數組的大小設置爲爲DEFAULT_CAPACITY=10 ,此時elementData.length=10,list.size()=0。

小結一下:構造函數走完之後,會構建出數組elementData和數量size。

補充一點:關於方法Arrays.copyOf(elementData, size, Object[].class),就是根據class的類型來決定是new 還是反射去構造一個泛型數組,同時利用native函數,批量複製元素至新數組中。源代碼如下:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        //根據class的類型來決定是new 還是反射去構造一個泛型數組
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
         //利用native函數,批量賦值元素至新數組中。
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

4、add()方法源碼分析


構造完成ArrayList對象之後嘗試往集合里加入元素,那麼新創建的對象添加元素是什麼流程呢?在add()方法處添加斷點,Debug走一下流程:

  • 進入斷點內部,看它調用的方法:
    /**
     * Appends the specified element to the end of this list.
     * 向列表末尾添加元素
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        //這裏調用了ensureCapacityInternal方法,傳入的參數是size+1  *此時size = 0
        //斷住,進去看代碼。
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
  • 方法內部調用了ensureCapacityInternal方法,傳的參數是數組和容量:
    private void ensureCapacityInternal(int minCapacity) {  //此時minCapacity=1
        //方法內部調用了calculateCapacity
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
  • 斷點進入calculateCapacity方法調用,繼續斷點進去看,這個就比較清晰了:
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        
        //利用判斷ArrayList對象是否是用默認構造函數初始化的
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //DEFAULT_CAPACITY = 10,minCapacity = 1,所以返回10
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
  • 該方法執行結束之後回到ensureCapacityInternal方法,繼續調用ensureExplicitCapacity方法:
    private void ensureCapacityInternal(int minCapacity) {
        //繼而調用ensureExplicitCapacity方法,傳入的參數是 10
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
  • 斷點進入ensureExplicitCapacity方法內部:(注意傳入的參數是10)
    private void ensureExplicitCapacity(int minCapacity) {
        //如果確定要擴容,會修改modCount 
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0) //minCapacity = 10 length =0
            grow(minCapacity);  //調用grow方法
    }
  • 經過if判斷成立,繼續調用了grow(minCapacity)方法,傳的參數是 10,斷點進入:
    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     * 增加容量,以確保它至少可以容納由最小容量參數指定的元素數目。
     * @param minCapacity the desired minimum capacity
     */

    //這裏纔是真正的給容器擴容
    private void grow(int minCapacity) {
        
        //oldCapacity:舊的容器容量(0)
        int oldCapacity = elementData.length;
        
        //默認擴容一半:新的容器容量 = 舊容器的容量+舊容器的容量*0.5
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        
        //如果新容量小於minCapacity的話,就把minCapacity賦值給newCapacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        
        //newCapacity 會與MAX_ARRAY_SIZE進行比較,不能超過Integer的最大值減8
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        
        //將elementData指向一個新分配的數組,容量爲10
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

可以看到這裏完成了容器(數組)的擴容操作,這段代碼執行完之後,會返回到:

這裏就將元素添加到了數組的0號位置,返回true,表示添加成功,至此,add()方法執行結束。

5、add(int index, E element) 方法源碼分析


此方法表示向list的指定位置添加元素,上代碼~:

public void add(int index, E element) {  //傳入的參數分別是指定的位置,要添加的元素
    
    rangeCheckForAdd(index);            //越界判斷 如果越界拋異常

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);    //將index開始的數據 向後移動一位
    elementData[index] = element;      //加入新數據
    size++;                            //元素個數+1
}

6、addAll(Collection<? extends E> c)方法源碼分析


此方法用於將一個集合裏的元素全部加入到當前集合中,代碼還算簡單:

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();     //將結合轉換爲數組
    int numNew = a.length;        //獲得數組長度
    ensureCapacityInternal(size + numNew); //判斷是否需要擴容
    System.arraycopy(a, 0, elementData, size, numNew);//複製數組至elementData中
    size += numNew;              //計算當前容器中的元素個數
    return numNew != 0;
}

7、addAll(int index, Collection<? extends E> c)方法源碼分析


此方法用於在指定位置插入整個集合:

public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);      //越界判斷 如果越界拋異常
    Object[] a = c.toArray();     //將結合轉換爲數組
    int numNew = a.length;        //獲得數組長度
    ensureCapacityInternal(size + numNew); //判斷是否需要擴容

    int numMoved = size - index;
    if (numMoved > 0)
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);//移動(複製)數組

    System.arraycopy(a, 0, elementData, index, numNew);//複製數組完成批量賦值
    size += numNew;
    return numNew != 0;
}

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