Java 集合系列1、細思極恐之ArrayList

peen發表於2018-05-04

1、ArrayList 概述

ArrayList 底層資料結構為 動態陣列 ,所以我們可以將之稱為陣列佇列。 ArrayList 的依賴關係:

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

Java 集合系列1、細思極恐之ArrayList

從依賴關係可以看出,ArrayList 首先是一個列表,其次,他具有列表的相關功能,支援快速(固定時間)定位資源位置。可以進行拷貝操作,同時支援序列化。這裡我們需要重點關注的是 AbstractLit 以及 RandomAccess 。這個類,一個是定義了列表的基本屬性,以及確定我們列表中的常規動作。而RandomAccess 主要是提供了快速定位資源位置的功能。

2、ArrayList 成員變數

  /**
     * Default initial capacity.陣列預設大小
     */
    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;

     // 陣列最大值
     private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
複製程式碼

在ArrayList 的原始碼中,主要有上述的幾個成員變數:

  • elementData : 動態陣列,也就是我們儲存資料的核心陣列
  • DEFAULT_CAPACITY:陣列預設長度,在呼叫預設構造器的時候會有介紹
  • size:記錄有效資料長度,size()方法直接返回該值
  • MAX_ARRAY_SIZE:陣列最大長度,如果擴容超過該值,則設定長度為 Integer.MAX_VALUE

擴充思考: EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA 都是兩個空的陣列物件,他們到底有什麼區別呢?我們在下一節講解構造方法的時候,會做詳細對比。

3、構造方法

ArrayList 中提供了三種構造方法:

  • ArrayList()
  • ArrayList(int initialCapacity)
  • ArrayList(Collection c)

根據構造器的不同,構造方法會有所區別。我們在平常開發中,可能會出現在預設構造器內部呼叫了 ArrayList(int capacity) 這種方式,但是ArrayList 中對於不同的構造器的內部實現都有所區別,主要跟上述提到的成員變數有關。

3.1 ArrayList()

在原始碼給出的註釋中這樣描述:構造一個初始容量為十的空列表

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
複製程式碼

從原始碼可以看到,它只是將 elementData 指向了 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的儲存地址,而 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 其實是一個空的陣列物件,那麼它為什麼說建立一個預設大小為10 的列表呢?

或者我們從別的角度思考一下,如果這個空的陣列,需要新增元素,會怎麼樣?

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  //確認內部容量
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        // 如果elementData 指向的是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的地址
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            設定預設大小 為DEFAULT_CAPACITY
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //確定實際容量
        ensureExplicitCapacity(minCapacity);
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 如果超出了容量,進行擴充套件
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    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);
    }
    
複製程式碼

上述程式碼塊比較長,這裡做個簡單的總結:

1、add(E e):新增元素,首先會判斷 elementData 陣列的長度,然後設定值

2、ensureCapacityInternal(int minCapacity):判斷 element 是否為空,如果是,則設定預設陣列長度

3、ensureExplicitCapacity(int minCapacity):判斷預期增長陣列長度是否超過當前容量,如果過超過,則呼叫grow()

4、grow(int minCapacity):對陣列進行擴充套件

回到剛才的問題:為什麼說建立一個預設大小為10 的列表呢?或許你已經找到答案了~
複製程式碼

3.2 ArrayList(int initialCapacity)

根據指定大小初始化 ArrayList 中的陣列大小,如果預設值大於0,根據引數進行初始化,如果等於0,指向EMPTY_ELEMENTDATA 記憶體地址(與上述預設構造器用法相似)。如果小於0,則丟擲IllegalArgumentException 異常。

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);
        }
    }
複製程式碼

擴充思考:為什麼這裡是用 EMPTY_ELEMENTDATA 而不是跟預設構造器一樣使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA ?有興趣的童鞋可以自己縣思考,經過思考的知識,才是你的~

3.3 ArrayList(Collection c)

將Collection<T> c 中儲存的資料,首先轉換成陣列形式(toArray()方法),然後判斷當前陣列長度是否為0,為 0 則只想預設陣列(EMPTY_ELEMENTDATA);否則進行資料拷貝。

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

3.4 總結

上述的三個構造方法可以看出,其實每個構造器內部做的事情都不一樣,特別是預設構造器與 ArrayList(int initialCapacity) 這兩個構造器直接的區別 ,我們是需要做一些區別的。

  • ArrayList():指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,當列表使用的時候,才會進行初始化,會通過判斷是不是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 這個物件而設定陣列預設大小。
  • ArrayList(int initialCapacity):當 initialCapacity >0 的時候,設定該長度。如果 initialCapacity =0,則指向 EMPTY_ELEMENTDATA 在使用的時候,並不會設定預設陣列長度 。

因此 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 與 EMPTY_ELEMENTDATA 的本質區別就在於,會不會設定預設的陣列長度。

4、新增方法(Add)

ArrayList 新增了四種新增方法:

  • add(E element)
  • add(int i , E element)
  • addAll(Collection<? extends E> c)
  • addAll(int index, Collection<? extends E> c)

4.1 add(E element)

首先看add(T t)的原始碼:

  public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 元素個數加一,並且確認陣列長度是否足夠 
        elementData[size++] = e;		//在列表最後一個元素後新增資料。
        return true;
    }
複製程式碼

結合預設構造器或其他構造器中,如果預設陣列為空,則會在 ensureCapacityInternal()方法呼叫的時候進行陣列初始化。這就是為什麼預設構造器呼叫的時候,我們建立的是一個空陣列,但是在註釋裡卻介紹為 長度為10的陣列。

4.2 add(int i , T t)

   public void add(int index, E element) {
    // 判斷index 是否有效
        rangeCheckForAdd(index);
    // 計數+1,並確認當前陣列長度是否足夠
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index); //將index 後面的資料都往後移一位
        elementData[index] = element; //設定目標資料
        size++;
    }
複製程式碼

這個方法其實和上面的add類似,該方法可以按照元素的位置,指定位置插入元素,具體的執行邏輯如下:

1)確保數插入的位置小於等於當前陣列長度,並且不小於0,否則丟擲異常

2)確保陣列已使用長度(size)加1之後足夠存下 下一個資料

3)修改次數(modCount)標識自增1,如果當前陣列已使用長度(size)加1後的大於當前的陣列長度,則呼叫grow方法,增長陣列

4)grow方法會將當前陣列的長度變為原來容量的1.5倍。

5)確保有足夠的容量之後,使用System.arraycopy 將需要插入的位置(index)後面的元素統統往後移動一位。

6)將新的資料內容存放到陣列的指定位置(index)上

4.3 addAll(Collection<? extends E> c)

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

addAll() 方法,通過將collection 中的資料轉換成 Array[] 然後新增到elementData 陣列,從而完成整個集合資料的新增。在整體上沒有什麼特別之初,這裡的collection 可能會丟擲控制異常 NullPointerException 需要注意一下。

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

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

與上述方法相比,這裡主要多了兩個步驟,判斷新增資料的位置是不是在末尾,如果在中間,則需要先將資料向後移動 collection 長度 的位置。

5、刪除方法(Remove)

ArrayList 中提供了 五種刪除資料的方式:

  • remove(int i)
  • remove(E element)
  • removeRange(int start,int end)
  • clear()
  • removeAll(Collection c)

5.1、remove(int i):

刪除資料並不會更改陣列的長度,只會將資料重陣列種移除,如果目標沒有其他有效引用,則在GC 時會進行回收。

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; // 讓指標最後指向空,進行垃圾回收
        return oldValue;
    }
複製程式碼

5.2、remove(E element):

這種方式,會在內部進行 AccessRandom 方式遍歷陣列,當匹配到資料跟 Object 相等,則呼叫 fastRemove() 進行刪除

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

fastRemove( ): fastRemove 操作與上述的根據下標進行刪除其實是一致的。

   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
    }
複製程式碼

5.3、removeRange(int fromIndex, int toIndex)

該方法主要刪除了在範圍內的資料,通過System.arraycopy 對整部分的資料進行覆蓋即可。

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

5.4、clear()

直接將整個陣列設定為 null ,這裡不做細述。

5.5、removeAll(Collection c)

主要通過呼叫:

    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++)
                //根據 complement 進行判斷刪除或留下
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // 進行資料整理
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }
複製程式碼

在retainAll(Collection c)也有呼叫,主要作用分別為,刪除這個集合中所包含的元素和留下這個集合中所包含的元素。

擴充思考

清楚ArrayList 的刪除方法後,再結合我們常用的刪除方式,進行思考,到底哪些步驟會出問題,我們通常會選擇變數列表,如果匹配,則刪除。我們遍歷的方式有以下幾種:

  • foreach():主要出現 ConcurrentModificationException 異常
  • for(int i;**;i++):主要出現相同資料跳過,可參考:https://blog.csdn.net/sun_flower77/article/details/78008491
  • Iterator 遍歷:主要出現 ConcurrentModificationException 可參考:https://www.cnblogs.com/dolphin0520/p/3933551.html

避免 ConcurrentModificationException 的有效辦法是使用 Concurrent包下面的 CopyOnWriteArrayList ,後續會進行詳細分析

6、toArray()

ArrayList提供了2個toArray()函式:

  • Object[] toArray()
  • T[] toArray(T[] contents)

呼叫 toArray() 函式會丟擲“java.lang.ClassCastException”異常,但是呼叫 toArray(T[] contents) 能正常返回 T[]。

toArray() 會丟擲異常是因為 toArray() 返回的是 Object[] 陣列,將 Object[] 轉換為其它型別(如如,將Object[]轉換為的Integer[])則會丟擲“java.lang.ClassCastException”異常,因為Java不支援向下轉型。

toArray() 原始碼:

    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }
    
複製程式碼

7、subList()

如果我們在開發過程中有需要獲取集合中的某一部分的資料進行操作,我們可以通過使用SubList() 方法來進行獲取,這裡會建立ArrayList 的一個內部類 SubList()。

SubList 繼承了 AbstractList,並且實現了大部分的 AbstractList 動作。

需要注意的是,SubList 返回的集合中的某一部分資料,是會與原集合相關聯。即當我們對Sublist 進行操作的時候,其實還是會影響到原始集合。 我們來看一下 Sublist 中的 add 方法:

  	public void add(int index, E e) {
        rangeCheckForAdd(index);
            checkForComodification();
            parent.add(parentOffset + index, e);
            this.modCount = parent.modCount;
            this.size++;
        }
複製程式碼

可以看到,Sublist 中的 加操作,其實還是呼叫了 parent(也就是原集合) 中的加操作。所以在使用subList方法時,一定要想清楚,是否需要對子集合進行修改元素而不影響原有的list集合。

總結

ArrayList總體來說比較簡單,不過ArrayList還有以下一些特點:

  • ArrayList自己實現了序列化和反序列化的方法,因為它自己實現了 private void writeObject(java.io.ObjectOutputStream s)和 private void readObject(java.io.ObjectInputStream s) 方法

  • ArrayList基於陣列方式實現,無容量的限制(會擴容)

  • 新增元素時可能要擴容(所以最好預判一下),刪除元素時不會減少容量(若希望減少容量,trimToSize()),刪除元素時,將刪除掉的位置元素置為null,下次gc就會回收這些元素所佔的記憶體空間。

  • 執行緒不安全

  • add(int index, E element):新增元素到陣列中指定位置的時候,需要將該位置及其後邊所有的元素都整塊向後複製一位

  • get(int index):獲取指定位置上的元素時,可以通過索引直接獲取(O(1))

  • remove(Object o)需要遍歷陣列

  • remove(int index)不需要遍歷陣列,只需判斷index是否符合條件即可,效率比remove(Object o)高

  • contains(E)需要遍歷陣列

  • 使用iterator遍歷可能會引發多執行緒異常

擴充思考

  • 擴充思考1、RandomAccess 介面是如何實現快速定位資源的?
  • 擴充思考2、EMPTY_ELEMENTDATA 與 DEFAULTCAPACITY_EMPTY_ELEMENTDA他的作用?
  • 擴充思考3、remove 方法存在的坑?
  • 擴充思考4:、ArrayList為什麼不是執行緒安全?

參考資料

http://www.cnblogs.com/skywang12345/p/3308556.html https://blog.csdn.net/daye5465/article/details/77971530 https://blog.csdn.net/daye5465/article/details/77971530

相關文章