java集合類(二)List之ArrayList

ArrayList概述:

  ArrayList是List接口基於數組的實現。它允許包括 null 在內的所有元素。每個ArrayList實例都有一個容量,該容量代表可以存儲元素的多少。每個ArrayList實例都有一個初始大小,當然你也可以通過構造方法指定初始大小。隨着向ArrayList中不斷添加元素,其容量也自動增長。當元素數量達到當前容量最大值時會導致數據向新數組的重新拷貝,因此,如果可預知數據量的多少,可在構造ArrayList時指定其容量。在添加大量元素前,應用程序也可以使用ensureCapacity操作來增加ArrayList實例的容量,這可以減少遞增式再分配的數量。 但是需要注意,此方法的實現不是同步的。如果多個線程同時訪問一個ArrayList實例,而其中至少一個線程從結構上修改了列表,那麼它必須保持外部同步。

底層探究

  • 1 初始容量
 private static final int DEFAULT_CAPACITY = 10;
  • 2 底層存儲
/**
 *Object類型數組
 */
transient Object[] elementData;
  • 3 構造方法
/**
 * 指定初始容量
 */
 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);
        }
    }

   /**
    * 默認構造,初始容量爲10
    */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

  /**
   * 使用一個集合初始化一個ArrayList
   */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
  • 4 存儲
      ArrayList提供了set(int index, E element)、add(E e)、add(int index, E element)、addAll(Collection
//設置指定位置元素的值(可理解爲更新操作)
set(int index, E element)
//往集合中添加元素,如果容量不足則擴容
add(E e)
//往指定位置插入元素,如果容量不足則先擴容。原先index及之後的元素均往後移動一位
add(int index, E element)
  • 5 刪除
//刪除指定位置的元素,原先index之後的元素均往前移動一位
public E remove(int index) ;
//移除此列表中首次出現的指定元素(如果存在),此操作需要遍歷數組
public boolean remove(Object o) {

以上介紹了ArrayList的增刪等操作,下面再來看下源碼

仔細觀察我們會發現還有兩個空數組

    private static final Object[] EMPTY_ELEMENTDATA = {};

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

這兩個空數組有什麼用呢?我們再把上面的兩個構造方法拿來看下:

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

    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);
        }
    }

我們可以看到,當使用默認構造方法時,直接將DEFAULTCAPACITY_EMPTY_ELEMENTDATA賦值給了elementData,當使用指定容量進行構造ArrayList時,如果initialCapacity=0,將EMPTY_ELEMENTDATA賦值給了elementData,這兩種方式構造出來的ArrayList初始容量均爲0,那麼爲什麼要使用兩個不同的空數組呢?想知道答案我們來看下add(E e);方法:

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

從上面代碼可以看到,在add一個元素時,纔對容量進行了操作,爲什麼要把擴容操作放在這裏呢?其實是防止你new出一個數組,但是不用,導致空間資源的浪費。回到上面的問題,我們看下ensureCapacityInternal方法:

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

看下這裏有個if判斷,當elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA時,對容量有一個賦值操作,minCapacity爲DEFAULT_CAPACITY與minCapacity的最大值。顯然當第一次add元素時minCapacity==1所以初始容量就爲DEFAULT_CAPACITY也就是10了。所以使用兩個不同的空數組的原因就是爲了保證:當我們使用默認構造方法對ArrayList進行構造時,初始容量爲10。

  下面再來看下ensureExplicitCapacitygrow方法:

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        //當最小所需容量大於當前數組容量時進行grow操作
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        //先嚐試新容量爲舊熔鍊個的1.5倍
        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);
    }

  可以看出,當最小所需容量大於當前數組容量時進行grow操作。先嚐試新容量爲舊熔鍊個的1.5倍,如果還不夠,那麼就使用minCapacity作爲當前擴充的容量大小。

  此外有沒有注意到ensureExplicitCapacity方法中有一個modCount++的操作,這個modCount是幹什麼用的呢?
  
  ArrayList的modCount是從類 java.util.AbstractList 繼承的字段:

protected transient int modCount

這個字段代表已從結構上修改此列表的次數。從結構上修改是指更改列表的大小(也就是元素個數發生了變化),或者打亂列表,從而使正在進行的迭代產生錯誤的結果。
此字段由 iterator 和 listIterator 方法返回的迭代器和列表迭代器實現使用。如果意外更改了此字段中的值,則迭代器(或列表迭代器)將拋出 ConcurrentModificationException 來響應 next、remove、previous、set 或 add 操作。在迭代期間面臨併發修改時,它提供了快速失敗 行爲,而不是非確定性行爲。

大家可以測試下下面這段程序:

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(10);
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            Integer integer = iterator.next();
            if (integer == 10)
                list.remove(integer);  //注意這個地方,此處換成list.add(integer)結果是一樣的
        }
    }

上面這段代碼會拋出如下異常:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at Main.main(Main.java:12)

爲何會報這個錯?看下ArrayListIterator源碼(有部分代碼未貼出):

    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

我們可以看到當做next()、remove()操作時,都要先checkForComodification()而這個方法做的事情就是判斷modCount 與expectedModCount是否相等。不相等就報錯。

發佈了35 篇原創文章 · 獲贊 58 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章