1. 程式人生 > >【原始碼分析】——Java集合之ArrayList

【原始碼分析】——Java集合之ArrayList

準備寫一個系列分析Java集合的原始碼,總體來說ArrayList原始碼除了個別方法其他都比較簡單,本篇分析ArrayList的原始碼先練練手~

一、概述和繼承關係

    ArrayList是基於動態陣列實現的,也就是說ArrayList中的物件被儲存在一個連續的陣列中。ArrayList中的元素可以被任意訪問,長度可以動態變化。ArrayList和Vector的用法類似,區別是:ArrayList是執行緒不安全的,當多個執行緒訪問同一個ArrayList集合時,程式需要手動保證該集合的同步性,而Vector則是執行緒安全的。

    下面看繼承關係

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

    ArrayList繼承了AbstractList抽象類,同時實現了多個介面,下面一一分析:

    1)ArrayList沒有直接實現List而是繼承了AbstractList,由AbstractList來實現List介面。這是因為在List介面中所有的方法都是抽象方法,都需要實現類去一一實現,而在AbstractList中有一部分方法是抽象方法而另一部分公用的方法已經提供了實現,這樣一來ArrayList在繼承AbstractList的時候就可以省去很多程式碼量去實現公用方法。然後再去實現一些自己特有的方法。一般來說一個類繼承自一個抽象類而抽象類又實現一個介面都是出於這樣的考慮。

    而對於ArrayList在已經繼承了AbstractList的情況下又實現了List介面的原因,則說法不一。有的人說是為了讓閱讀方便,但是據說ArrayList的作者自己說過這其實是一個mistake,他本以為預先這樣寫以後可能會用到,但後來沒有用到也沒有什麼別的影響就沿用至今。

    2)下面實現的介面:

    RandomAccess介面。用來說明這個類可以被任意訪問,實現了此介面的類的物件在遍歷的時候使用for迴圈會比使用Iterator效率更高(例如LinkedList沒有實現RandomAccess介面,使用Iterator實現遍歷效率更高)。

    Cloneable介面。實現了這個介面就可以呼叫clone方法了。

    Serializable介面。表示類可以被序列化。

二、構造方法

    ArrayList有如下三個構造方法

    分別表示建立一個指定大小的ArrayList;建立一個空的ArrayList;建立一個ArrayList並且讓其中的元素和引數中的Collection內的元素一一對應(相當於複製Collection到ArrayList中)。

    在分析構造方法之前先看一下ArrayList中的公共屬性:

    //如欄位名所示
    private static final long serialVersionUID = 8683452581122892189L;

    //預設capacity值,capacity用來管理陣列大小
    private static final int DEFAULT_CAPACITY = 10;

    //空物件陣列
    private static final Object[] EMPTY_ELEMENTDATA = {};

    //預設空物件陣列
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    //元素陣列
    transient Object[] elementData; // non-private to simplify nested class access

    private int size;
    
    //size的最大值
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

ps:其中,為什麼已經有一個EMPTY_ELEMENTDATA的情況下要再加一個DEFAULTCAPACITY_EMPTY_ELEMENTDATA呢?官方文件中的解釋如下

/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/

    意思就是說這個常量陣列是用來和EMPTY_ELEMENTDATA區分,使得當在一個空陣列中加入第一個元素時讓我們知道應該給元素陣列擴充多少的空間。

    另一處註釋也需要額外注意一下

/**
*(...)
* Any empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/

    任何一個空的ArrayList陣列並且其中的elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,這時如果給這個空陣列加入一個元素,那麼我們會將這個ArrayList的大小擴充到DEFAULT_CAPACITY也就是10。這樣一解釋,上面的問題也就更容易明白了。

/**
 * 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
 */

    另外對於 MAX_ARRAY_SIZE 為什麼等於 Integer.MAX_VALUE - 8 官方文件中這樣解釋道:有些虛擬機器預留了一些位置用於存放陣列的頭資訊(header words),如果size超過了這個數值,會出現OOM:請求的陣列大小超出虛擬機器限制。

    下面轉入正題我們一一來看這三個構造方法:

1)無引數構造方法

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

    最簡單的構造方法,直接將預設的空元素陣列賦值給elementData。

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的Object陣列給elementData;如果等於0那麼將EMPTY_ELEMENTDATA空元素陣列賦給elementData。(這裡又一次解釋了兩個空元素陣列之間的區別)其實可以這樣理解,如果我們使用無參構造方法建立了集合,一定程度上表明我們並不清楚最終會用到多大的空間,所以程式會將DEFAULTCAPACITY_EMPTY_ELEMENTDATA賦給elementData,以便在首次插入元素的時候,直接將陣列擴充到DEFAULT_CAPACITY的大小,也可以避免後續操作再反覆進行擴充。

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 {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

    這個構造方法看引數應該能想到它的作用,就是將一個已經存在的Collection物件複製到一個新的ArrayList中去。首先讓c通過toArray()方法轉為陣列後賦給elementData,然後進行判斷如果elementData的大小為0則將上面的EMPTY_ELEMENTDATA賦給elementData;否則根據方法中的註釋顯示:

 // c.toArray might (incorrectly) not return Object[] (see 6260652)

    c.toArray()方法有可能不會返回一個Object[](其中的6260652應該是內部的bug編號,6開頭說明是從jdk6中出現的)。因為可能會轉化失敗,所以就要做一次修正處理,也就有了下面的Arrays.copyOf()。

    copyOf()方法如下:

    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

    可以看到方法在最後呼叫了System.arraycopy這個本地方法,用來複制陣列。也就是說copyOf()方法會按照輸入的引數要求返回一個數組,後面還會在遇到此方法的不同引數的過載方法,這裡只要知道呼叫它是用來對陣列進行修正,將轉化後的陣列修正為Object陣列即可。

三、主要方法

1、插入add()方法

    add方法有以下四個,下面依次分析

public boolean add(E e)
public void add(int index, E element)
public boolean addAll(Collection<? extends E> c)
public boolean addAll(int index, Collection<? extends E> c) 

  1)boolean add(E e)

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

    此方法將一個元素e插入到ArrayList集合中,並且預設插入到末尾。可以看到方法先執行了ensureCapacityInternal(size+1),然後將e放入到了elementData中。下面我們來看ensureCapacityInternal方法都做了什麼

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

    可以看到,程式會首先判斷elementData,如果是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(說明集合通過無參構造方法建立),直接比較minCapacity和DEFAULT_CAPACITY的大小,然後將較大的賦給minCapacity。

    然後方法內呼叫了ensureExplicitCapacity(),引數為minCapacity,我們看一下ensureExplicitCapacity()都做了什麼

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

    這個方法首先使modCount自增,這個變數是AbstractList中的變數,用來記錄List被修改的次數(ModifiedCount)。然後比較minCapacity和elementData.length的大小,如果minCapacity大就執行grow方法

    grow方法是擴充陣列的關鍵方法,如下:

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1); //新的newCapacity大小為舊值的1.5倍(oldCapacity+1/2的oldCapacity)
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity; //擴充1.5倍還不夠的話,直接設為minCapacity
        if (newCapacity - MAX_ARRAY_SIZE > 0) //newCapacity大於最大值限制時
            newCapacity = hugeCapacity(minCapacity); //呼叫hugeCapacity賦最大值
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity); //返回一個newCapacity大小的並且copy了elementData原內容的陣列,當作更新後的陣列賦給elementData
    }

    最後簡單看一下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;
    }

    這個方法很簡單就是當minCapacity超過了MAX_ARRAY_SIZE的時候那就把Integer.MAX_VALUE賦值給它,其實也並沒有加大多少,如果再超過就會溢位。上面呼叫了這麼多層,其實只做了一件事就是給ArrayList擴容,以便元素能夠新增進去。而分析清楚了這個過程,對於後面其他方法的理解也會有幫助。

2)void add(int index, E element)

    public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

    分析過上面的add(E e)方法在看這個就簡單多啦~這個方法是將元素插入到指定位置。首先判斷是否越界,然後老樣子先將集合擴容,然後不同於上面方法的是,這裡不是插入到末尾而是插入到index的位置。這時需要先將index位置之後的所有元素後移一位,然後再插入新元素。於是呼叫System.arraycopy方法,將elementData中從index位置起的長度為size - index的陣列元素複製到elementData中的index + 1的位置。

    之後在elementData[index]處插入新元素即可。

3)boolean addAll(Collection<? extends E> c)

    此方法用於將Collection中的所有元素新增到ArrayList中

public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray(); //c轉化為陣列
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount //對集合擴容
        System.arraycopy(a, 0, elementData, size, numNew); //將a的從0開始的長度為numNew的元素copy到elementData中從size開始的位置
        size += numNew; //size記錄增加
        return numNew != 0;
    }

4)boolean addAll(int index, Collection<? extends E> c)

此方法用於將Collection中的所有元素新增到ArrayList中的指定位置

    public boolean addAll(int index, Collection<? extends E> c) {
        if (index > size || index < 0) //判斷是否越界
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew); //擴容集合

        int numMoved = size - index; //要移動的元素數
        if (numMoved > 0) //numMoved>0即插入的位置不在末尾
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved); //先將numMoved個元素進行後移

        System.arraycopy(a, 0, elementData, index, numNew); //插入元素
        size += numNew;
        return numNew != 0;
    }

    可以看到,其實4個add方法的實現過程都很簡單,主要進行了兩步:1、將集合進行擴容;2、插入元素。而對集合擴容主要用到前面分析的一連串方法,最後使用grow來實現;插入元素則都圍繞著System.arraycopy()這個方法來實現移動以及插入的操作。

    下面盜兩張別人部落格上看到的圖,來解釋擴容的過程。文章結尾會註明出處:

    呼叫無參構造方法建立集合,當加入第一個元素時,集合自動擴容至10。

  List<Integer> lists = new ArrayList<Integer>();
  lists.add(8);

              

    呼叫ArrayList(int)建立集合,當插入第一個元素時,程式發現集合容量足夠不需要擴容,於是直接插入。

  List<Integer> lists = new ArrayList<Integer>(6);
  lists.add(8);

 

2、刪除remove()方法

1)E remove(int index)

刪除指定位置的元素,需要將刪除位置之後的所有元素向前移動。

    public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        modCount++; //修改數記錄
        E oldValue = (E) elementData[index]; //記錄index位置原來的資料

        int numMoved = size - index - 1; //計算需要向前移動的元素數
        if (numMoved > 0) //numMoved>0表示刪除的不是尾部的元素
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved); //
        elementData[--size] = null; //集合尾部的重複元素設為null,使GC回收多餘物件

        return oldValue;
    }

2)boolean remove(Object o)

    刪除ArrayList中和Object o相等的元素,這裡的相等指的是用equals方法得到的結果為true。

    public boolean remove(Object o) {
        if (o == null) { //如果o為空
            for (int index = 0; index < size; index++) //遍歷集合
                if (elementData[index] == null) { //找到elementData中所存的null元素
                    fastRemove(index);  //刪除index位置的元素
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) { //找到elementData中與o相等的元素
                    fastRemove(index); //刪除該位置的元素
                    return true;
                }
        }
        return false; //若沒有匹配到則返回false
    }

    其中分為當o是否為null這兩種情況,也說明了ArrayList中是可以儲存null的。我們還看到,這裡多次呼叫了fastRemove()方法,其實fastRemove方法和remove(int index)基本一樣,只是免去了一些判斷和計算。

    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; // 集合尾部的重複元素設為null,使GC回收多餘物件
    }

3)void removeRange(int fromIndex, int toIndex)

    protected void removeRange(int fromIndex, int toIndex) {
        if (toIndex < fromIndex) {
            throw new IndexOutOfBoundsException("toIndex < fromIndex");
        }
        modCount++;
        int numMoved = size - toIndex; //移動元素的數量
        System.arraycopy(elementData, toIndex, elementData, fromIndex,
                         numMoved);  //移動元素
        // clear to let GC do its work
        int newSize = size - (toIndex-fromIndex); 
        for (int i = newSize; i < size; i++) { //批量刪除多餘的重複元素,置null等待GC回收多餘物件
            elementData[i] = null;
        }
        size = newSize;
    }

4)boolean removeAll(Collection<?> c) 和 boolean retainAll(Collection<?> c)

    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c); //若為空丟擲空指標異常
        return batchRemove(c, false);
    }
    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }

    這兩個方法都用到了batchRemove方法,下面分析一下這個方法

    private boolean batchRemove(Collection<?> c, boolean complement) { //complement引數用來指示操作,為true則,為false則批量刪除
        final Object[] elementData = this.elementData; //用ArrayList與c做比較
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement) //若c中存在/不存在elementData[r]元素
                    elementData[w++] = elementData[r]; //記錄兩集合公有/ArrayList中獨有的元素
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            // 這裡要注意有可能在呼叫c.contains()的時候出現異常,則有可能r != size
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r); // 把r位置之後的所有元素接到w位置之後
                w += size - r; // w位置為當前的最後一個元素的位置+1,之前執行過w++所以多1(除去後面部分的重複元素)
            }
            if (w != size) { // 需要刪除多餘元素的情況(若兩個集合完全相同/完全不相同則w=size)
                for (int i = w; i < size; i++)
                    elementData[i] = null; //批量將重複的元素置null,使GC回收多餘物件
                modCount += size - w;
                size = w; // size設為w,因為w是最後一個元素位置+1所以可以作為size
                modified = true;
            }
        }
        return modified;
    }

    經過上面的分析我們可以發現,removeAll和retainAll兩個方法的作用正好是相反的,重新命名也可以看出來。retainAll是留下全部的,也就是說從ArrayList集合中留下和Collection共有的那部分,結果就是留下了兩個集合的交集;removeAll是刪除全部,也就是從ArrayList集合中刪除Collection中共有的那部分,換句話說就是留下ArrayList所獨有的那部分。

5)boolean removeIf(Predicate<? super E> filter)

這個刪除方法是刪除滿足一定條件的元素(也就是引數中的filter過濾出來的元素)。官方註釋中提到任何由於Predicate而丟擲的異常都會導致這個集合不發生改變。

    public boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        int removeCount = 0; //記錄刪除元素數
        final BitSet removeSet = new BitSet(size); //使用BitSet記錄要刪除的元素的位置
        final int expectedModCount = modCount; //期望改變數,用來進行同步控制
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) { //遍歷集合,將符合刪除條件的元素記錄在removeSet中
            @SuppressWarnings("unchecked")
            final E element = (E) elementData[i]; 
            if (filter.test(element)) {
                removeSet.set(i); //記錄位置,設為true
                removeCount++;
            }
        }
        if (modCount != expectedModCount) { //同步控制
            throw new ConcurrentModificationException();
        }

        final boolean anyToRemove = removeCount > 0; //設定返回值,是否刪除元素
        if (anyToRemove) {
            final int newSize = size - removeCount; //最終的集合大小
            for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
                i = removeSet.nextClearBit(i);  //返回下一個false位的索引,false位代表此位置的元素沒有被標記刪除
                elementData[j] = elementData[i]; //更新集合
            }
            for (int k=newSize; k < size; k++) {
                elementData[k] = null;  //刪除多餘的元素,使GC回收多餘物件
            }
            this.size = newSize;
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
            modCount++;
        }

        return anyToRemove;
    }

    這裡出現兩個概念Predicate介面和BitSet類。其中實現Predicate介面的物件可以完成一些條件過濾的操作,這裡用來判斷集合中的元素那些需要刪除;BitSet用一個64位的long型資料記錄每一位的資訊,初始時64位上都儲存false,當執行set(i)的時候,第i位上的值更改為true。

3、其他方法

1)void replaceAll(UnaryOperator<E> operator)

    替換集合中的所有元素,替換為引數operator。其中operator實現了UnaryOperator介面,這個介面是一個函式式介面,這裡不做過多討論,簡單來說呼叫其中的operator方法可以得到一個類中定義好的物件。

    public void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final int expectedModCount = modCount;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            elementData[i] = operator.apply((E) elementData[i]); //遍歷集合,每次賦值呼叫operator的apply方法,將返回的值賦到當前位置
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

2)void sort(Comparator<? super E> c) 

    將集合元素進行排序,其中引數是一個實現Comparator介面的物件,這個介面可以自定義一些比較大小的方法、判斷相等的方法等等,然後sort()方法會根據引數中定義的規則對集合進行排序。

    public void sort(Comparator<? super E> c) {
        final int expectedModCount = modCount;
        Arrays.sort((E[]) elementData, 0, size, c);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

3)另外還有一些get()、set()、indexOf()、clear()等等的方法原始碼較為簡單,功能也可以顧名思義,在此不一一分析。

四、ArrayList總結

  1. ArrayList可以存放null元素,本質上是一個elementData陣列。
  2. ArrayList區別於陣列的地方在於能夠自動擴充套件大小,在首次新增元素的時候會根據情況不同選擇是否擴容至10;每次自動擴容都會擴容至原來的1.5倍。這些都是隻有閱讀了原始碼才會瞭解到的。
  3. 在向集合中插入元素或者刪除元素時,發生移位的元素使用的是複製陣列的方法,並且在移位後要及時將空餘的位置設為null,便於GC去回收多餘的物件。
  4. ArrayList的本質是陣列,所以它在資料的查詢方面會很快,而在插入刪除這些方面,效能下降很多,有移動很多資料才能達到應有的效果。
  5. ArrayList實現了RandomAccess,所以在遍歷它的時候推薦使用for迴圈。
  6. Java8中定義了很多函式式介面,這也在ArrayList中有許多體現,包括替換集合元素、遍歷集合元素、排序等操作都用到了函式式介面,所以有空要補充一下Java函數語言程式設計的知識。

本文部分內容參考以下文章,在此感謝