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

一、ArrayList 概述

ArrayList 底層數據結構爲 動態數組 ,因此咱們能夠將之稱爲數組隊列。 ArrayList 的依賴關係:html

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

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

二、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 都是兩個空的數組對象,他們到底有什麼區別呢?咱們在下一節講解構造方法的時候,會作詳細對比。安全

三、構造方法

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

  • 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 的列表呢?dom

或者咱們從別的角度思考一下,若是這個空的數組,須要添加元素,會怎麼樣?函數

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

上述代碼塊比較長,這裏作個簡單的總結:ui

一、add(E e):添加元素,首先會判斷 elementData 數組的長度,而後設置值

二、ensureCapacityInternal(int minCapacity):判斷 element 是否爲空,若是是,則設置默認數組長度

三、ensureExplicitCapacity(int minCapacity):判斷預期增加數組長度是否超過當前容量,若是過超過,則調用grow()

四、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 的本質區別就在於,會不會設置默認的數組長度。

四、添加方法(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 長度 的位置。

五、刪除方法(Remove)

ArrayList 中提供了 五種刪除數據的方式:

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

5.一、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.二、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.三、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.四、clear()

直接將整個數組設置爲 null ,這裏不作細述。

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 ,後續會進行詳細分析

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

七、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遍歷可能會引起多線程異常

拓展思考

  • 拓展思考一、RandomAccess 接口是如何實現快速定位資源的?
  • 拓展思考二、EMPTY_ELEMENTDATA 與 DEFAULTCAPACITY_EMPTY_ELEMENTDATA的做用?
  • 拓展思考三、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

相關文章
相關標籤/搜索