讀懂ArrayList這一篇就夠了

微信公衆號Duffy說碼 若是你以爲這篇文章對你有幫助,歡迎關注java

注:本文全部代碼來自JDK1.8面試

ArrayList簡介

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

ArrayList繼承了AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable接口api

java.lang.Object
   java.util.AbstractCollection<E>
      java.util.AbstractList<E>
         java.util.ArrayList<E>
複製代碼

List是動態數組,並容許操做全部元素,包括null。 size,isEmpty,get,set,iterator和listIterator方法都是以恆定時間運行。數組

每一個ArrayList實例都有一個容量大小。此容量是列表數組中可以存儲元素的個數。它始終是大於等於元素個數的。當ArrayList中添加元素時,其容量會自動增長。在經過ensureCapacity方法添加大量元素以前,能夠手動設置ArrayList實例的容量,這樣會減小從新分配的次數。安全

請注意,ArrayList實現是不一樣步。若是多個線程同時訪問ArrayList實例,而且至少有一個線程在結構上修改了列表,則必須進行同步。 (結構修改是指添加或刪除一個或多個元素的操做,或顯式調整後備數組的大小;僅設置元素的值不是結構修改。)這一般經過同步一些天然封裝的對象來實現。名單。若是不存在此類對象,則應使用Collections.synchronizedList方法「包裝」該列表。bash

經常使用函數

面試常問問題

一、如何擴容的?微信

二、線程安全嗎?如何確保線程安全數據結構

三、如何初始化?多線程

四、查找、添加、刪除元素時間複雜度?併發

五、ArrayList與LinkedList的比較

源碼分析

/** * 默認初始化容量 */
    private static final int DEFAULT_CAPACITY = 10;

    /** * 用於沒有實例的共享空數組實例 */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /** * 用於默認大小空實例的共享空數組實例 * 區別於EMPTY_ELEMENTDATA, 當第一個元素添加進來可知該如何膨脹 */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /** * ArrayList的底層數據結構 * 任何空ArrayList元素類型爲DEFAULTCAPACITY_EMPTY_ELEMENTDATA * 當第一個元素添加進來將擴展爲DEFAULT_CAPACITY * */
    transient Object[] elementData; //非私有,以簡化嵌套類訪問
複製代碼

注意:transient關鍵字是指在採用Java默認的序列化機制的時候,被該關鍵字修飾的屬性不會被序列化,說明此屬性不採用默認序列化方法,由於本身實現了序列化和反序列化(writeObjectreadObject)。

/**
     * 包含的元素個數,與容量作區別
     */
    private int size;
    /**
     * 最大數組大小爲Integer.MAX_VALUE - 8,爲何要 -8 ?
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
複製代碼

初始化函數

/** * 構建一個指定容量的空列表 */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {//建立輸入大小的數組
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {//初始容量爲0,初始化一個空數組
            this.elementData = EMPTY_ELEMENTDATA;
        } else {//當輸入的初始化容量不符合要求,拋出IllegalArgumentException
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /** * 構建一個容量爲10的空列表 */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /** * 構建一個包含指定集合的全部元素的列表,元素順序按照集合迭代器返回的順序 */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // 將原來集合中元素向上轉換爲Object類型
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 當集合元素爲0,初始化一個空數組
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

    /** * 把ArrayList的容量減少到元素數目大小,可最小化存儲 */
    public void trimToSize() {
        modCount++;//算結構性改變
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }
複製代碼

查找元素

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

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

先判斷要查找的下標是否大於等於數組大小(從0開始存數據),若沒有返回該位置元素,不然拋出下標越界異常

public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

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

    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;
    }
複製代碼

一、首先判斷輸入元素是否爲null,避免空指針異常(ArrayList能夠存null元素)

二、indexOf方法從頭至尾遍歷,lastIndexOf方法從尾到頭遍歷

三、找到則返回相應位置下標,爲大於等於0的整數,沒有找到相應元素則返回-1

添加元素

假設在一羣人在排隊中,忽然來了一我的,正常的話是排到隊尾的,至關於add(E e);可是這我的可能有急事就會選擇插隊,那麼他找個位置插進去,那麼他插進去位置後面的人都須要日後走一步纔會產生空間,這就至關於add(int index, E element)

//在列表末尾添加元素
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // modCount加1
        elementData[size++] = e;
        return true;
    }

    //在列表中任意位置添加元素
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);  

        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
複製代碼

一、不只檢查輸入的index是否大於數組大小,還判斷是否小於0

二、size+1,其中modCount加1

三、向後移動一部分元素,爲要添加的元素騰出空間

四、在所在下標添加元素

五、list中元素個數加1

//指定集合全部元素添加到list集合中,添加了返回true,不然返回false
    public boolean addAll(Collection<? extends E> c) {// E表明父類 ,?表明 子類 
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }
複製代碼

其中各個參數意義以下:

System.arraycopy(src, srcPos, dest, destpoS,length);
複製代碼

Object src : 從哪複製 -- 源數組.

int srcPos : 源數組從哪一個下標開始複製.

Object dest : 複製到哪 -- 目的數組.

int destPos :粘貼到開始下標的位置.

int length :將要被複制的元素個數.

//
    private void ensureCapacityInternal(int minCapacity) {
        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);
    }
複製代碼

若是minCapacity - elementData.length > 0,說明數組已經放滿,須要擴容,調用grow()

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
複製代碼

一、獲取當前數組長度oldCapacity

二、新的長度爲oldCapacity + (oldCapacity >> 1),即將oldCapacity右移一位,至關於除以2,新容量是舊容量的1.5倍

三、若是newCapacity - minCapacity < 0,則新容量設置爲minCapacity

四、若是新容量比容許的最大數組容量都要大,則新容量設置爲虛擬機容許的最大值

五、將elementData放入新容量數組中

在調用add方法時,首先調用ensureCapacityInternal(int minCapacity)

刪除元素

//刪除指定位置上的元素
    public E remove(int index) {
        rangeCheck(index);//1

        modCount++;//2
        E oldValue = elementData(index);//3

        int numMoved = size - index - 1;//4
        if (numMoved > 0)//5
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; //6

        return oldValue;//7
    }
複製代碼

一、判斷是否小於數組個數,不然拋異常結束

二、modCount須要增長1

三、舊值須要返回,記錄舊值

四、計算移動的個數,下圖舉例5-2-1=2

五、數組進行左移操做,即刪除了要刪除的元素

六、最後位置設置爲null,使垃圾回收

七、返回舊值

//刪除指定的元素
    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;
    }

    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,避免空指針異常,而後從頭至尾遍歷找對應元素,找到刪除返回true,不然返回false。

二、fastRemove(int index)與上面的remove(int index)相似,但少了邊界檢測,由於說明已經在邊界內找到了相應元素。

三、關鍵modCount++;,下面細節分析中會用到

將list對象轉換爲數組對象

public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

    public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }
複製代碼

toArray()做爲數組對象和集合對象轉化的橋樑,將返回包含list中全部元素的一個新分配的數組。

使用建議

與LinkedList相比

時間複雜度分析

一、 ArrayList基於索引結構,在中間插入或刪除一個元素會致使列表中一部分元素移動, 適合查找操做,不適合中間插入、刪除操做; LinkedList基於鏈表結構,在中間插入或刪除元素的開銷是固定的, 所以適合頻繁插入、刪除操做。

二、在ArrayList的添加元素,當達到閾值會致使數組進行從新分配的容量。

三、ArrayList的空間浪費主要體如今list列表的結尾預留必定的容量空間;而LinkedList的空間花費則體如今它的每個元素都須要消耗至關的空間,包括指向指針

與Array相比

ArrayList內部封裝了一個Object類型的數組,本質與數組沒有差異,一些方法內部都是調用array方法實現的。

細節分析

fail-fast爲快速失敗機制,爲Java集合的一種錯誤檢測機制

此類的iteratorlistIterator方法返回的迭代器是fail-fast的:若是在建立迭代器以後的任什麼時候候對列表進行結構修改,好比經過迭代器本身的remove或add方法,迭代器將拋出ConcurrentModificationException。所以,在併發修改的狀況下,迭代器會快速中止操做,而不是在將來的未肯定時間發生非肯定性行爲的風險,並且即便不是在多線程環境,若是單線程違反了規則,一樣也會拋出異常。 插入開發手冊

在foreach循環中進行元素刪除,爲何會引發fail-fast機制呢?由於foreach循環中集合遍歷是經過iterator進行的。

經過iterator直接運行,固然拋出一樣的異常結果。

經過堆棧信息能夠獲得異常發生在調用鏈第14行, String next = iterator.next();中調用了 iterator().checkForComodification()方法,而異常就是這個方法中拋出的,看下拋出異常的緣由。

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

說明發生了modCount != expectedModCount,才拋出的異常。接下來咱們來分析modCountexpectedModCount這兩個變量 插入圖expectedMod

  • modCount是AbstractList中一個成員變量,被ArrayList繼承,表示該集合中實際被修改的次數。

  • expectedModCount 是ArrayList中內部類Itr中的成員變量,表示這個迭代器指望該集合被修改的次數,只有經過迭代器對集合進行操做,該值纔會改變。

  • Itr是一個Iterator的實現,ArrayList.iterator方法得到的迭代器就是Itr的實例。

private class Itr implements Iterator<E> {
        int expectedModCount = modCount;
}
複製代碼

之間的關係以下:

class ArrayList{
    private int modCount;
    public void add();
    public void remove();
    private class Itr implements Iterator<E> {
        int expectedModCount = modCount;
    }
    public Iterator<E> iterator() {
        return new Itr();
    }
}
複製代碼

是什麼致使modCount != expectedModCount,經過以前分析源碼可知fastRemove(int index)modCount加1,但此時expectedModCount沒有加1,才致使兩個數不相等。

簡單來講集合遍歷是經過iterator進行的,可是元素的add/remove倒是使用類中本身的方法嗎,所以混淆在了一塊兒,致使iterator在遍歷的時候,當一個元素經過本身方法添加或刪除時,就會拋出一個異常,提醒用戶,可能發生了併發修改。

知道了之後咱們能夠選擇正確的方法在遍歷的時候刪除一些元素

一、使用普通for循環,此時沒有使用iterator

二、迭代器iterator的remove()方法

參考:

Java Standard Ed. 8 docs.oracle.com/javase/8/do…

Holis 《爲何阿里巴巴禁止在 foreach 循環裏進行元素的 remove/add 操做》

持續輸出高質量乾貨,歡迎關注公衆號Duffy說碼

相關文章
相關標籤/搜索