淺析Java源碼之ArrayList

來源:互聯網
上載者:User

標籤:相關   開始   limit   指定   序列化   fast   bsp   內部使用   網上   

  面試題經常會問到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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.