學習JDK1.8集合源碼之--ArrayList

 

參考文檔: https://cloud.tencent.com/developer/article/1145014html

https://segmentfault.com/a/1190000018578944java

http://www.importnew.com/9928.html算法

https://blog.csdn.net/zero__007/article/details/52166306segmentfault

1. ArrayList簡介

  ArrayList底層基於數組實現的一種線性數據結構,經過數組的索引原理實現了快速查找,是非線程安全的。數組

  因爲數組建立時必須制定容量並且不可更改,ArrayList經過自動擴容的方式彌補了數組容量不可更改的弊端,但同時也帶了性能方面的隱患。緩存

2.ArrayList繼承關係

  ArrayList繼承自AbstractList,實現了List、RandomAccess、Cloneable、java.io.Serializable接口。  安全

  實現了全部List接口的操做,並ArrayList容許存儲null值。除了沒有進行同步,ArrayList基本等同於Vector。在Vector中幾乎對全部的方法都進行了同步,但ArrayList僅對writeObject和readObject進行了同步,其它好比add(Object)、remove(int)等都沒有同步。數據結構

  1. AbstractList提供了List接口的默認實現(個別方法爲抽象方法)。
  2. List接口定義了列表必須實現的方法。
  3. 實現了RandomAccess接口:提供了隨機訪問功能。RandmoAccess是java中用來被List實現,爲List提供快速訪問功能的。在ArrayList中,咱們便可以經過元素的序號快速獲取元素對象;這就是快速隨機訪問。
  4. 實現了Cloneable接口:能夠調用Object.clone方法返回該對象的淺拷貝。
  5. 實現了 java.io.Serializable 接口:能夠啓用其序列化功能,能經過序列化去傳輸。未實現此接口的類將沒法使其任何狀態序列化或反序列化。序列化接口沒有方法或字段,僅用於標識可序列化的語義。

3. ArrayList實現

1. 核心屬性    

 transient Object[] elementData;
 private int size;
View Code

  elementData是ArrayList中用來存儲數據的底層數組,size表明數組中存儲的元素個數。app

  有個關鍵字須要解釋:transient。Java的serialization提供了一種持久化對象實例的機制。當持久化對象時,可能有一個特殊的對象數據成員,咱們不想用serialization機制來保存它。爲了在一個特定對象的一個域上關閉serialization,能夠在這個域前加上關鍵字transient。dom

  ArrayList在序列化的時候會調用writeObject,直接將size和element寫入ObjectOutputStream;反序列化時調用readObject,從ObjectInputStream獲取size和element,再恢復到elementData。

  爲何不直接用elementData來序列化,而採用上訴的方式來實現序列化呢?緣由在於elementData是一個緩存數組,它一般會預留一些容量,等容量不足時再擴充容量,那麼有些空間可能就沒有實際存儲元素,採用上訴的方式來實現序列化時,就能夠保證只序列化實際存儲的那些元素,而不是整個數組,從而節省空間和時間。

2. 構造函數

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

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;
     }
}
View Code

  ArrayList有三個構造函數,一個無參構造,一個指定容量的有參構造,一個指定集合的有參構造。

  無參構造會建立一個的列表,內部數組容量爲0,當調用add方法添加元素時會擴容成默認容量10(爲什麼網上都說是構造一個默認初始容量爲10的空列表???)。

  指定容量的有參構造會建立一個內部數組爲指定容量大小的空列表。

  指定集合的有參構造會建立一個包含指定collection的元素的列表,這些元素按照該collection的迭代器返回它們的順序排列的。

3. 存儲元素

  當對ArrayList進行元素添加的時候,都會檢查底層數組的容量是否足夠,如果不夠則進行自動擴容,每次對數組進行增刪改的時候都會增長modCount(繼承自AbstractList的屬性,用來統計修改次數),添加單個元素時通常會擴容1.5倍[oldCapacity + (oldCapacity >> 1)],添加集合時,若是(原數組的長度 + 添加的集合長度 > 原數組的長度的1.5倍)則會擴容至(原數組的長度 + 添加的集合長度)

  存儲元素分爲三種類型:追加,插入,替換,其中第二種和第三種類型都會檢查下標是否越界(根據size屬性而不是數組的長度)

  (1)追加:這種方式最經常使用,直接添加到數組中最後一個元素的後面。

  public boolean add(E paramE)
  {
    ensureCapacityInternal(this.size + 1);
    this.elementData[(this.size++)] = paramE;
    return true;
  }

  public boolean addAll(Collection<? extends E> paramCollection)
  {
    Object[] arrayOfObject = paramCollection.toArray();
    int i = arrayOfObject.length;
    ensureCapacityInternal(this.size + i);
    System.arraycopy(arrayOfObject, 0, this.elementData, this.size, i);
    this.size += i;
    return i != 0;
  }
View Code

  (2)插入:當調用下面這兩個方法向數組中添加元素或集合時,會先查找索引位置,而後將元素添加到索引處,最後把添加前索引後面的元素追加到新元素的後面。 

   public void add(int index, E element) {
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

    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;
    }
View Code

  (3)替換:調用該方法會將index位置的元素用新元素替代。

     public void set(E e) {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.set(lastRet, e);
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
View Code

4. 元素讀取

       public E get(int index) {
            rangeCheck(index);
            checkForComodification();
            return ArrayList.this.elementData(offset + index);
        }
View Code

5. 元素刪除

  ArrayList提供了5種方式的刪除功能。以下: 

  (1)romove(int index),首先是檢查範圍,修改modCount,保留將要被移除的元素,將移除位置以後的元素向前挪動一個位置,將list末尾元素置空(null),返回被移除的元素。 

    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; // clear to let GC do its work

        return oldValue;
    }
View Code

  (2)remove(Object o),這裏爲了防止equals方法空指針異常,對remove對象爲空的狀況作了特殊處理,而後遍歷底層數組找到與remove對象相同的元素調用fastRemove後返回true,fastRemove的邏輯和romove(int index)的邏輯一致,只是少了範圍檢查以及沒有返回值。

    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; // clear to let GC do its work
    }
View Code

  (3)removeRange(int fromIndex, int toIndex),修改modCount,把toIndex後面的元素經過System.arraycopy複製到toIndex位置後面,計算移除後的數組大小newSize,數組中下標大於等於newSize的元素所有置爲空,方便垃圾回收,重置size屬性。

    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;
    }
View Code

  (4)removeAll(Collection<?> c),首先檢查參數,爲空則拋出異常,而後調用batchRemove(c, false),在batchRemove中,先把底層數組元素賦給一個final修飾的局部變量elementData,而後遍歷elementData,把elementData中除了c包含的元素從下標爲0開始依次存入elementData進行重排序,最後經過(r != size)判斷try塊中是否出現過異常,出現過異常則把elementData中未遍歷過的元素所有複製到下標爲w後面,經過(w != size)判斷原數組是否已經改變,若是已改變則修改modCOunt,將下標爲w後面的元素所有置爲空,重置size屬性,把modified屬性設爲true表明移除成功並返回。

    public boolean removeAll(Collection<?> c) {
        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++)
                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) {
                // 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;
    }
View Code

  (5)removeIf(Predicate<? super E> filter),在JDK1.8中,Collection以及其子類新加入了removeIf方法,做用是經過lambda表達式移除集合中符合條件的元素。如移除List<Integer>中全部大於10的元素: list.removeIf(Integer -> Integer > 10);

    @Override
    public boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        // figure out which elements are to be removed
        // any exception thrown from the filter predicate at this stage
        // will leave the collection unmodified
        int removeCount = 0;
        final BitSet removeSet = new BitSet(size);
        final int expectedModCount = modCount;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            @SuppressWarnings("unchecked")
            final E element = (E) elementData[i];
            if (filter.test(element)) {
                removeSet.set(i);
                removeCount++;
            }
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }

        // shift surviving elements left over the spaces left by removed elements
        final boolean anyToRemove = removeCount > 0;
        if (anyToRemove) {
            final int newSize = size - removeCount;
            for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
                i = removeSet.nextClearBit(i);
                elementData[j] = elementData[i];
            }
            for (int k=newSize; k < size; k++) {
                elementData[k] = null;  // Let gc do its work
            }
            this.size = newSize;
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
            modCount++;
        }

        return anyToRemove;
    }
View Code

6. 調整數組容量

  (1)ensureCapacity(int minCapacity),首先自定義一個變量,若是內部數組elementData爲空則賦值爲0,不然賦值爲10,若是數組須要的容量大於自定義變量的值,則調用ensureExplicitCapacity(minCapacity)。ensureExplicitCapacity方法會先修改modCount,而後當須要的容量大於elementData的長度時調用grow(minCapacity),grow方法是最終進行擴容算法的方法。grow方法內先定義了兩個變量oldCapacity(當前數組長度)和newCapacity(擴容後的數組長度),newCapacity的值是oldCapacity的1.5倍(oldCapacity >> 1 左移1位至關於除以2),若是須要擴容的容量大於newCapacity,則把newCapacity賦值爲minCapacity(也就是說ArrayList擴容不必定每次擴容都是1.5倍,ArrayList內部默認擴容1.5倍,但因爲ensureCapacity是一個public方法,咱們能夠外部手動調用,當須要向ArrayList中添加大量元素時,咱們能夠提早根據須要添加的元素數量調用ensureCapacity,根據須要添加的元素數量提早調用ensureCapacity來進行手動擴容,避免遞增式自動擴容反覆調用Arrays.copyOf帶來的性能損耗,提升程序效率),而後就是判斷須要擴容的容量是否大於int的最大值,若是超過最大值則拋出內存溢出異常,若是大於(int最大值 - 8),小於等於int最大值,則把newCapacity設爲int最大值,最後也是最核心的一步,經過Arrays.copyOf進行數組的擴容。

    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        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);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
View Code

  (2)trimToSize(),這個方法很簡單,就是把底層數組的長度調整爲元素實際個數大小,這個方法主要是爲了防止ArrayList進行擴容時產生的空間浪費。因爲elementData的長度會被拓展,size標記的是其中包含的元素的個數。因此會出現size很小但elementData.length很大的狀況,將出現空間的浪費。通常只有當肯定了ArrayList的元素再也不增長時進行調用。

    public void trimToSize() {
        modCount++;
        int oldCapacity = elementData.length;
        if (size < oldCapacity) {
            elementData = Arrays.copyOf(elementData, size);
        }
    }
View Code

7. 轉爲靜態數組的兩種方法

  (1)直接將底層數組拷貝一份大小爲size的新數組並返回

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

  (2)若是傳入數組的長度小於size,返回一個新的數組,大小爲size,類型與傳入數組相同。所傳入數組長度與size相等,則將elementData複製到傳入數組中並返回傳入的數組。若傳入數組長度大於size,除了複製elementData外,還將把返回數組的第size個元素置爲空。這裏須要注意的是參數中的T類型要與ArrayList中存儲的數據類型一致,不然會出現ArrayStoreException。

    // 返回ArrayList的模板數組。所謂模板數組,便可以將T設爲任意的數據類型
    public <T> T[] toArray(T[] a) {
        // 若數組a的大小 < ArrayList的元素個數;
        // 則新建一個T[]數組,數組大小是「ArrayList的元素個數」,並將「ArrayList」所有拷貝到新數組中
        if (a.length < size)
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        // 若數組a的大小 >= ArrayList的元素個數;
        // 則將ArrayList的所有元素都拷貝到數組a中。
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }
View Code

8.實現了Cloneable接口,進行數據淺拷貝

  若是ArrayList中存的是引用類型數據,則把拷貝後的ArrayList中的對象的屬性修改後,源ArrayList中對應的對象的屬性也會改變,固然移除和添加元素沒有影響,如果基本數據類型則沒有這問題。

    // 克隆函數
    public Object clone() {
        try {
            ArrayList<E> v = (ArrayList<E>) super.clone();
            // 將當前ArrayList的所有元素拷貝到v中
            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();
        }
    }
View Code

9.實現Serializable 接口,啓用其序列化功能

    // java.io.Serializable的寫入函數
    // 將ArrayList的「容量,全部的元素值」都寫入到輸出流中
    private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();
        // 寫入「數組的容量」
        s.writeInt(elementData.length);
        // 寫入「數組的每個元素」
        for (int i = 0; i < size; i++)
            s.writeObject(elementData[i]);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

    // java.io.Serializable的讀取函數:根據寫入方式讀出
    // 先將ArrayList的「容量」讀出,而後將「全部的元素值」讀出
    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        // Read in size, and any hidden stuff
        s.defaultReadObject();
        // 從輸入流中讀取ArrayList的「容量」
        int arrayLength = s.readInt();
        Object[] a = elementData = new Object[arrayLength];
        // 從輸入流中將「全部的元素值」讀出
        for (int i = 0; i < size; i++)
            a[i] = s.readObject();
    }
View Code

10. 實現了RandomAccess接口,啓用隨機訪問

  實際上RandomAccess是一個空的接口,它到的做用是作一個標記,用來區分List的實現類是否支持隨機訪問。事實上不一樣實現方式的不一樣遍歷方式性能差別較大,如ArrayList使用for循環遍歷比用iterator迭代器要快,而LinkedList使用iterator迭代器遍歷比for循環要快的多,因此List不一樣子類須要採用不一樣遍歷方式以提升性能。但是咱們如何區分List的實現子類是ArrayList仍是LinkedList呢?這時候RandomAccess接口就派上用場了,用instanceof方法來判斷該子類是否實現了RandomAccess接口,進而更好的判斷集合是否ArrayList或者LinkedList,從而可以更好選擇更優的遍歷方式,提升性能!

4. ArrayList常見問題

1. ArrayList的默認初始長度是多少?最大長度是多少?

  ArrayList默認長度爲10,但新建立的ArrayList容量其實默認爲0,首次添加元素時會自動擴容至默認容量10;最大長度爲int的最大值(2147483647),而不是ArrayList內部定義的常量MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)

2. ArrayList是如何擴容的?

  ArrayList底層是經過ensureCapacity方法來進行擴容的(具體擴容實現前面已經說過了),這個方法通常是在向ArrayList中添加元素時首先調用以確保容量足以容納新增的元素,同時這個方法咱們也能夠手動調用來進行擴容,通常在知道數據量較大的狀況下提早手動擴容以免頻繁擴容帶來的性能損耗。大致的擴容思路就是每次添加元素時,檢查底層數組容量是否足夠,足夠則直接添加,如果不夠則擴容1.5倍,如果要添加的也是一個集合,則取這個集合的大小與當前ArrayList的大小之和與底層數組長度的1.5倍二者之間的最大值進行擴容。

3. ArrayList擴容後是否會自動縮容?若是不能怎樣進行縮容?

   ArrayList只能自動擴容,不能自動縮容。若是須要進行縮容,能夠調用ArrayList提供的trimToSize()方法。

4. ArrayList底層數組擴容時是如何保證高效複製數組的?

  表面上是調用Arrays.copyOf()方法,其實是Arrays.copyOf()經過調用System.arraycopy()方法複製數組的,但貌似並不高效~~。

5. 什麼狀況下你會使用ArrayList?何時你會選擇LinkedList?

  這個問題實際上考察的是ArrayList與LinkedList的區別,也就是數組與鏈表的區別。數組因爲索引的存在,查找較快,而鏈表查找時須要進行遍歷,因此當查找操做較多的狀況下用ArrayList更合適;若是查找較少,增刪較多的狀況選擇LinkedList則更爲合適,由於在ArrayList中增長或者刪除某個元素,一般會調用System.arraycopy方法,這是一種極爲消耗資源的操做。

6. 當傳遞ArrayList到某個方法中,或者某個方法返回ArrayList,何時要考慮安全隱患?如何修復安全違規這個問題呢?

  當array被當作參數傳遞到某個方法中,若是array在沒有被複制的狀況下直接被分配給了成員變量,那麼就可能發生這種狀況,即當原始的數組被調用的方法改變的時候,傳遞到這個方法中的數組也會改變。下面的這段代碼展現的就是安全違規以及如何修復這個問題。

pulic void setArr(String[] arr){
    this.arr = arr;
}
View Code

  修復這個安全隱患:

public void setArr(String[] newArr){
    if(newArr == null)
        this.arr = new String[0];
    else
        this.arr = Arrays.copyOf(newArr, newArr.length);
}
View Code

7. 如何複製某個ArrayList到另外一個ArrayList中去?寫出你的代碼? 

  下面就是把某個ArrayList複製到另外一個ArrayList中去的幾種技術:

  1. 使用clone()方法,好比ArrayList newArray = oldArray.clone();
  2. 使用ArrayList構造方法,好比:ArrayList myObject = new ArrayList(myTempObject);
  3. 使用Collection的copy方法。

  注意1和2是淺拷貝(shallow copy)。

8. 在索引中ArrayList的增長或者刪除某個對象的運行過程?效率很低嗎?解釋一下爲何?

  在ArrayList中增長或者是刪除元素,要調用System.arraycopy這種效率很低的操做

相關文章
相關標籤/搜索