1. 程式人生 > >淺析Java源碼之ArrayList

淺析Java源碼之ArrayList

city ans st2 不用 定義 結構 count als 拋出異常

  面試題經常會問到LinkedList與ArrayList的區別,與其背網上的廢話,不如直接擼源碼!

  文章源碼來源於JRE1.8,java.util.ArrayList

  既然是淺析,就主要針對該數據結構的內部實現原理和部分主要方法做解釋,至於I/O以及高級特性就暫時略過。

變量/常量

  首先來看定義的(靜態)變量:

class ArrayList2<E>
    //extends AbstractList<E>
    //implements RandomAccess, Cloneable, java.io.Serializable 
{
    private
static final long serialVersionUID = 8683452581122892189L; private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; private int size; }

  這裏在一開始定義了6個變量,其中第一個跟序列化相關不用管,其余5個依次解釋一下:

DEFAULT_CAPACITY:代表容器ArrayList的初始化默認大小

Object[] EMPTY_ELEMENTDATA:一個空數組,在某些方法調用後(例如removeAll)會用到

Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA:默認空數組,未傳參初始化時默認為這個

Object[] elementData:保存著當前ArrayList的內容,該變量被標記為序列化忽略對象

int size:很明顯,當前ArrayList大小

  需要註意的是,其中幾個被標記為static final變量。

構造函數

  看完變量,接下來看構造函數部分,構造函數有3個重載版本,分別闡述如下。

1、無參版本

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

  如果不傳任何參數直接初始化一個ArrayList,會得到上面默認的空數組。

2、int版本

    public ArrayList(int initialCapacity) {
        // 正常情況會初始化一個指定大小的數組
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        }
        // 傳0在實現上與不傳是一樣的
        else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } 
        // 亂傳就拋異常
        else {
            throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
        }
    }

  這基本上最普遍的情況,可以看出內部實現就是普通的數組。

3、Collection版本

    public ArrayList(Collection<? extends E> c) {
        // 將集合轉換為數組並賦給本地變量
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 傳空集合相當於空數組
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

  如果初始化傳入一個集合,會將此集合作為ArrayList的初值。

  這裏存在一個bug,即toArray方法返回的不一定是Object,雖然默認情況下是,但是如果被重寫就不一定了。

  詳細問題可見另一位的博客:http://blog.csdn.net/gulu_gulu_jp/article/details/51457492

  如果返回不是Object類型,會做向上轉型。

方法

  接下來看看常用的方法。

首先是get/set方法:

    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }
    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

  可以看出十分簡單暴力,首先會進行範圍檢查,然後返回/設置對應index的元素。

  簡單看一下rangeCheck:

    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

  也十分簡單,主要是判斷所給的索引index是否大於數組的大小size,否則拋出異常。

  獲取對應的值時,沒有直接用elementData[index],而是用了一個方法elementData(),看著有點混,看一下方法定義:

    E elementData(int index) {
        return (E) elementData[index];
    }

  方法其實只是對取出來的值進行了類型轉換,保證了返回類型的準確。

接下來是add/remove方法

  這兩個方法都有重載版本,但是並不復雜,而且都用的比較多。

  首先看add的一個參數版本,會在尾部插入給定元素。

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

  稍微講下源碼註釋的modCount,這個變量來源於java.util.AbstractList,專門用來計算容器被改動的次數,對於我這種菜鳥使用者來說沒啥用。

  這裏會首先檢測下容器的容量,然後在尾部加入元素,並將size加1。

  看看ensureCapacityInternal方法:

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

  原來這是一個皮包函數,當數組元素為空時,會進行參數修正,由於容器的默認大小為10,所以不會對10以下的容量進行檢測。

  修正後,將10或者比10大的形參傳入ensureExplicitCapacity進行檢測:

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

  這看起來也像個皮包方法,不過並不是,如果當前容器大小已經達到上限,會調用grow進行擴容:

    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

  其實這裏的形參名字我覺得不是特別好,應該叫最小所需容量,即minNeededCapacity。

  這裏首先會獲取當前容器大小,並進行擴容,這裏的擴容是這樣算的:

  oldCapacity + (oldCapacity >> 1)

  也就是如果之前為10,那麽新容量為10 + Math.floor(10/2) = 15。

  得到新容量後,會與傳進來的 所需容量進行對比,如果還不夠,那就幹脆取所需容量為新容量。

  第二個if是判斷擴容後的容量是否大於最大(數組可達)整數,看下MAX_ARRAY_SIZE變量定義就明白了:

    /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

  這裏的註釋有必要看一眼,簡單講就是有些JVM會在數組中加入一些東西,所以實際上數組大小是比理論上小一點的。這個很容易理解的,比如電腦硬盤,容量100G,可用容量其實會打個折扣,一個道理的。

  為了完整,所以也看一下hugeCapacity函數的內部實現:

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

  參數檢測挺好玩,內部使用的函數還怕傳入負數。

  這裏會將所需容量與最大可用安全容量作比較,如果實在沒辦法,就將容量設置為最大可用容量,至於這裏會不會出問題我也不知道。

  回到grow方法,得到新的容量後,會調用Arrays.copyOf方法,這個方法是包內另一個類的方法,內部實現是調用System.arraycopy直接進行內存復制,效率很高,最後返回一個新數組,size為加大後的容量。

  

  接下來看第二個重載的add方法:

    public void add(int index, E element) {
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

  這裏的檢測不太一樣,多了一步,不過看一眼方法就明白了:

    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

  由於這個重載方法是插入,所以需要進行數值檢測,如果插入索引大於數組大小或者小於0,拋個異常。

  接下來是常規的容量檢測。

  下一步的方法就是之前提到的System.arraycopy,該方法會將索引+1後面的元素全部復制到源數組,舉個簡單的例子:

  如果原數組為[1,2,3,4],假設索引為1,經過這一步,數組會變為[1,2,2,3,4]。

  最後是將對應索引的值賦為給定值,size++。

  可以看出,在數組中間插入一個元素是非常耗時的,會變動索引後面的每一個數組元素。

  接下來是remove,這個方法也有2個重載,一個是刪除給定索引,一個是刪除給定元素:

    public E remove(int index) {
        rangeCheck(index);
        modCount++;
        E oldValue = elementData(index);
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
        return oldValue;
    }

  沒啥好講的,忽略檢測,一句話概括就是將對應索引-1的所有元素復制到原數組,然後size-1,並將末尾元素置null讓GC進行回收,最後返回刪除元素。

    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

  這個是第二個重載,對null進行了特殊處理,這個奇怪的東西只能用==來進行比較。

  總體來講就是遍歷數組,如果找到了匹配元素,進行fastRemove,刪除成功返回true,否則返回false。

  這個快速刪除也沒什麽稀奇的:

    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

  跟第一個重載的remove很相似,只是移除了範圍檢測與返回值的處理,更快一些。

  其余的方法大多數是上面的變種,沒什麽研究的必要了,有興趣的可以自行閱讀源碼。

淺析Java源碼之ArrayList