我的jdk源碼(十一):ArrayList類

一、概述

    ArrayList類是AbstractList的子類,實現了具體的add(), set(), remove()等方法。它是一個可調整大小的數組可以用來存放各種形式的數據。

二、源碼分析

    (1) 類的聲明  

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

    ArrayList類繼承自AbstractList類,實現了List、RandomAccess、Cloneable、Serializable四個接口。具體作用如下:

        * 繼承AbstractList類,可以自己實現add(), set(), remove()等方法,還可以用AbstractList類的modCount來實現快速失敗。

        * 實現List接口,一是爲了增加可讀性,清晰看到實現的接口,二是降低維護成本,如果AbstractList類不實現List了,ArrayList類也不受影響。

        * RandomAccess接口是標記接口,表示實現它的類支持快速隨機訪問。在使用循環遍歷List的時候應該判斷下這個集合是否是RandomAccess的實例,如果是就是用for循環來操作,如果不是就是使用iterator迭代器來操作。隨機訪問一般是通過index下標訪問,行爲類似數組的訪問。而順序訪問類似於鏈表的訪問,通常爲迭代器遍歷。以List接口及其實例爲例。ArrayList是典型的隨機訪問型,而LinkedList則是順序訪問型。

        * Cloneable接口也是克隆標記接口,表示此類可以被克隆,此類的實例可以調用clone()方法;未實現Cloneable接口的類的實例調用clone()方法會報錯,在Object類中已經定義。

        * Serializable接口是序列化標記接口,表示此類可以被序列化到內存中。目的是爲類可持久化,比如在網絡傳輸或本地存儲,爲系統的分佈和異構部署提供先決條件。

    (2) 成員變量

    //serialVersionUID適用於java序列化機制。簡單來說,JAVA序列化的機制是通過判斷類的serialVersionUID來驗證的版本一致的。
    private static final long serialVersionUID = 8683452581122892189L;
    //定義初始容量10
    private static final int DEFAULT_CAPACITY = 10;
    //定義一個空數組,使用場景爲初始容量爲10或者構造函數中傳入空集合時,下面源碼中可看到具體使用場景。
    private static final Object[] EMPTY_ELEMENTDATA = {};
    //定義一個空數組,使用場景是調用無參構造函數時,下面源碼中可看到具體使用場景。
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    //transient修飾的Object類型的數組,表示此數組不支持序列化。也是ArrayList的內部容器。
    transient Object[] elementData; // non-private to simplify nested class access
    //數組的實際元素數量
    private int size;
    //定義一個數組對象最大容量,只是用來做比較,ArrayList能達到的最大容量還是Integer.MAX_VALUE
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    (3) 構造方法

    //無參構造函數,直接將DEFAULTCAPACITY_EMPTY_ELEMENTDATA賦值給內部容器elementData 
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    //指定容量的構造函數
    public ArrayList(int initialCapacity) {
        //如果指定容量大於0,直接賦值一個指定容量的Object數組
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //若指定容量等於0,則將EMPTY_ELEMENTDATA賦值給內部容器elementData
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //如果指定容量小於0,則拋出異常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    //指定集合的構造函數
    public ArrayList(Collection<? extends E> c) {
        //直接將集合轉爲數組賦值給內部容器elementData
        elementData = c.toArray();
        //先將elementData的大小賦值給size,再判斷size是否爲0
        if ((size = elementData.length) != 0) {
            //如果轉換後的elementData不是Object[]類型,則調用Arrays.copyOf()方法賦值
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            //如果size爲0,則將EMPTY_ELEMENTDATA賦值給內部容器elementData
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

    由源碼看出,每次size判斷爲0時,都是將EMPTY_ELEMENTDATA賦值給了內部容器elementData。

    (4) add()方法

    public boolean add(E e) {
        //第一步就是確定容量
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //把元素添加到末尾的位置
        elementData[size++] = e;
        return true;
    }

    private void ensureCapacityInternal(int minCapacity) {
        //如果此時,是無參構造函數創建的ArrayList添加元素,那麼設置DEFAULT_CAPACITY與minCapacity中大的那個數作爲容量。也就是add()第一個元素的時候,容量就爲10了。
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        //父類的屬性,用於標記修改次數,在併發修改時,用於快速失敗
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            //動態擴容核心邏輯方法
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        //獲取原容量oldCapacity 
        int oldCapacity = elementData.length;
        //獲取新容量newCapacity爲原容量oldCapacity的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果新容量newCapacity還是小於目標容量minCapacity,那麼設置新容量就爲目標容量minCapacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果新容量newCapacity大於MAX_ARRAY_SIZE,也就是Integer.MAX_VALUE-8,則調用hugeCapacity()方法
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //擴容成功後,複製舊數組到新數組中
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        //如果minCapacity < 0,也就是minCapacity已經超過了int的最大值Integer.MAX_VALUE,則拋出內存溢出錯誤
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //判斷目標容量minCapacity是否大於MAX_ARRAY_SIZE,如果大於了,就返回Integer.MAX_VALUE,沒有超過就返回MAX_ARRAY_SIZE
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

    add()方法的擴容機制,總結起來分爲以下兩步:

        * 添加一個元素,沒超過默認容量大小10的時候,容量不變;超過後變爲原來的1.5倍。

        * 添加多個元素,超過了默認容量大小是,先擴容到原容量的1.5倍, 然後與目標容量做比較,誰大就用誰。然後進行邊界判斷,就是判斷擴容後的容量是否已經大於了MAX_ARRAY_SIZE:如果小於MAX_ARRAY_SIZE,則還是用新擴容的容量;如果大於了MAX_ARRAY_SIZE,則需要對目標容量進行判斷。a) 目標容量小於0,其實就是已經越界了,那麼就直接拋出錯誤;b) 目標容量如果也大於了MAX_ARRAY_SIZE,則將新容量設置爲Integer.MAX_VALUE;c) 若目標容量小於MAX_ARRAY_SIZE,則設置新容量爲MAX_ARRAY_SIZE。

    值得一提的是,ArrayList的元素插入操作,不是將對應位置的元素覆蓋,而是將該位置的元素已經後面的元素全部向後移動,騰出目標位置給目標元素存放。所以這種操作的費時費力程度與插入元素離末尾位置的距離成正比,也就是說操作的位置裏離末尾越遠就越費時費力。

    (5) add()系列的其他方法

    //在指定座標處添加元素
    public void add(int index, E element) {
        //下標校驗調用方法rangeCheckForAdd(),判斷是否越界
        rangeCheckForAdd(index);
        //下面的操作和add()方法基本一致
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
    //批量插入一個集合中的所有元素
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }
    //從指定座標index處開始,批量插入一個集合中的所有元素
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        //如果插入的位置是在中間
        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;
    }
    //插入操作時,進行下標校驗
    private void rangeCheckForAdd(int index) {
        //插入目標位置index不能大於元素個數。
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    操作步驟與add()方法也大致相同,但是在指定下標index處插入指定集合c時,需要先將index以及後面的元素向後移動c的元素個數,以保證能存下目標集合c所有的元素。

    (6) set()方法修改元素

    public E set(int index, E element) {
        //慣例檢查下標是否越界
        rangeCheck(index);

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

    private void rangeCheck(int index) {
        //此處的判斷規則是,需要修改的下標index只能小於容器元素個數size,和此容器容量無關。與add()方法的判斷規則完全不同。此處也沒有判斷index>0,是因爲elementData()方法數組取值時,可以直接拋出異常
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    //生成錯誤信息
    private String outOfBoundsMsg(int index) {
        return "Index: "+index+", Size: "+size;
    }
    //返回數組對應下標下的元素
    E elementData(int index) {
        return (E) elementData[index];
    }

    (7) remove()方法移除元素

    public E remove(int index) {
        //慣例檢查是否越界,與set()用的一個方法
        rangeCheck(index);
        //監測修改次數,用於快速失敗
        modCount++;
        //獲取原來index座標下的元素
        E oldValue = elementData(index);
        //判斷移除的元素位置是否不在末尾
        int numMoved = size - index - 1;
        //numMoved>0表示移除的元素位置是不在末尾的
        if (numMoved > 0)
            //移除指定位置元素,並將後面的元素向前移
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //如果在末尾,直接將最後一位的元素設置爲null,清除後讓GC回收
        elementData[--size] = null; // clear to let GC do its work
        //返回舊值
        return oldValue;
    }

    remove()方法不會引起數組的容量變化。

    (8) remove()系列的其他方法

    //移除指定元素
    public boolean remove(Object o) {
        //判斷參數元素o是否爲null,爲null採用==比較,不爲null採用equals比較
        if (o == null) {
            for (int index = 0; index < size; index++)
                //如果相同則刪除,然後return了,所以remove(Object o)方法只會刪除集合第一個與傳入對象相同的元素。
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            //不爲null採用equals比較
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    //與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; // clear to let GC do its work
    }
    //移除fromIndex到toIndex的元素
    protected void removeRange(int fromIndex, int toIndex) {
        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++) {
            elementData[i] = null;
        }
        size = newSize;
    }
    //移除集合c中包含的所有元素
    public boolean removeAll(Collection<?> c) {
        //調用Object的工具類Objects的requireNonNull()方法進行非空校驗
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }
    //批量移除
    private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                //如果elementData[r]這個元素不包含在集合c中,則將這個元素從座標0開始往後放,也就是將不移除的元素前移
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                //把座標w到size的元素全部置爲null,以便GC回收
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

    需要注意的是,需要遍歷ArrayList的時候,只能用它實例的迭代器進行迭代,不能直接採用for循環等直接比那裏,會拋出異常。

    (9) trimToSize()瘦身方法

    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

    這個方法會將自動擴容後,沒有被元素填充的位置清除掉,也就是這個方法執行後,size應該與容量相等。

    (10) isEmpty()方法

    public boolean isEmpty() {
        return size == 0;
    }

    判斷ArrayList類的實例是否沒有元素,但不是null,判斷的是size是否爲0。

    (11) contains()方法

    //判斷是否含有目標元素O
    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
    //返回元素o所在下標
    public int indexOf(Object o) {
        //判斷是否爲null,如果是則採用==判斷
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            ////判斷是否爲null,如果不是則採用equals()方法判斷
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        //不存在則返回-1
        return -1;
    }

    indexOf()方法也只是返回找到的第一個符合條件的元素的下標,後面還有也不管。

    (12) lastIndexOf()方法

    public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

    lastIndexOf()方法也只是返回找到的第一個符合條件的元素的下標,但是是從後往前找,前面還有也不管。

    (13) clone()方法

    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

    ArrayList的本質是維護了一個Object的數組,所以克隆也是通過數組的複製實現的,屬於淺複製。想要深克隆,需要循環克隆每個對象元素。

    (14) clear()方法

    public void clear() {
        modCount++;

        //循環設置每一個原有元素爲null
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

    還有其他簡單的方法就不講解了。ArrayList類和AbstractList類類似,也有兩個內部類 Iterator, ListIterator ,它們都是迭代器的實現類,分別爲 Itr,ListItr;還有內部類SubList類,都可以查看《我的jdk源碼(八):AbstractList List家族的骨架實現類》 中的詳細介紹。這裏再做一下簡單的概括:

    * Iterator類型的迭代器Itr對象,只能向後遍歷,可以調用Itr.remove()移除元素,但是不能新增元素。

    * ListIterator類型的迭代器ListItr對象相比Iterator類型的Itr對象要多一個向前遍歷的功能,並且在Iterator的基礎上,還能調用add()方法添加新的元素。

    * SubList返回的是原集合的視圖,也就是說,如果對 subList 出來的集合進行修改或新增操作,那麼原始集合也會發生同樣的操作。想要獨立出來一個集合就需要重新new一個新的對象,代碼如下:

List<String> subList = new ArrayList<>(list.subList(0, 1));

三、總結

    學習ArrayList類主要是關注它的動態擴容,以及size和容量間的區別。突然想到一題分享給大家,源碼如下:

    ArrayList l = new ArrayList<String>(2);
    l.add("1");
    l.set(1,"2");

    提問:這段代碼執行後,l中變成什麼內容?

    答案:這是陷阱題,set()方法執行的時候會報錯,雖然初始化指定了容量爲2,但是add()方法執行後,size是1,set()方法是改變目標座標的值,而座標爲1的地方是沒有東西的,所以會拋出下標越界的異常。可以結合上文中的set()方法源碼加深理解。

    好了,這就是ArrayList類的內容,敬請期待下一篇《我的jdk源碼(十二):LinkedList類》。更多精彩內容,掃碼關注我的微信公衆號【Java覺淺】,獲取第一時間更新!

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