搞懂 Java ArrayList 源碼

ArrayList 源碼分析

不知道各位朋友,還記得開工前制定的學習目標麼? 有沒有一直爲了那個目標廢寢忘食呢?繼 搞懂 Java 內部類 後開始探索總結 Java 集合框架源碼的知識,但願能給本身夯實基礎,也但願能爲本身實現目標更近一步。html

ArrayList 源碼分析思路

ArrayList 是咱們 App 開發中經常使用的 Java 集合類,從學習 Java 開始咱們基本上就對它每天相見了,可是經過探索ArrayList 源碼,咱們將會把它從普通朋友變成知根知底的老朋友,本文將從如下幾部分開始分析 ArrayListjava

  1. ArrayList 概述
  2. ArrayList 的構造函數,也就是咱們建立一個 ArrayList 的方法
  3. ArrayList 的添加元素的方法, 以及 ArrayList 的擴容機制
  4. ArrayList 的刪除元素的經常使用方法
  5. ArrayList 的 改查經常使用方法
  6. ArrayList 的 toArray 方法
  7. ArrayList 的遍歷方法,以及常見的錯誤操做即產生錯誤操做的緣由

ArrayList 概述

ArrayList的基本特色

  1. ArrayList 底層是一個動態擴容的數組結構
  2. 容許存放(不止一個) null 元素
  3. 容許存放重複數據,存儲順序按照元素的添加順序
  4. ArrayList 並非一個線程安全的集合。若是集合的增刪操做須要保證線程的安全性,能夠考慮使用 CopyOnWriteArrayList 或者使用 collections.synchronizedList(List l)函數返回一個線程安全的ArrayList類.

ArrayList 的繼承關係

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

ArrayList 的繼承關係來看, ArrayList 繼承自 AbstractList,實現了List<E>, RandomAccess, Cloneable, java.io.Serializable 接口。面試

  • 其中 AbstractListList<E> 是規定了 ArrayList 做爲一個集合框架必須具有的一些屬性和方法,ArrayList 自己覆寫了基類和接口的大部分方法,這就包含咱們要分析的增刪改查操做。算法

  • ArrayList 實現 RandomAccess 接口標識着其支持隨機快速訪問,查看源碼能夠知道RandomAccess 其實只是一個標識,標識某個類擁有隨機快速訪問的能力,針對 ArrayList 而言經過 get(index)去訪問元素能夠達到 O(1) 的時間複雜度。有些集合類不擁有這種隨機快速訪問的能力,好比 LinkedList 就沒有實現這個接口。編程

  • ArrayList 實現 Cloneable 接口標識着他能夠被克隆/複製,其內部實現了 clone 方法供使用者調用來對 ArrayList 進行克隆,但其實現只經過 Arrays.copyOf 完成了對 ArrayList 進行「淺複製」,也就是你改變 ArrayList clone後的集合中的元素,源集合中的元素也會改變,對於深淺複製我之後會單獨整理一篇文章來說述這裏再也不過多的說。segmentfault

  • 對於 java.io.Serializable 標識着集合可被被序列化。數組

咱們發現了一些有趣的事情,除了List<E> 之外,ArrayList 實現的接口都是標識接口,標識着這個類具備怎樣的特色,看起來更像是一個屬性。安全

ArrayList 的構造方法

在說構造方法以前咱們要先看下與構造參數有關的幾個全局變量:bash

/**
 * ArrayList 默認的數組容量
 */
 private static final int DEFAULT_CAPACITY = 10;

/**
 * 這是一個共享的空的數組實例,當使用 ArrayList(0) 或者 ArrayList(Collection<? extends E> c) 
 * 而且 c.size() = 0 的時候講 elementData 數組講指向這個實例對象。
 */
 private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 另外一個共享空數組實例,再第一次 add 元素的時候將使用它來判斷數組大小是否設置爲 DEFAULT_CAPACITY
 */
 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 真正裝載集合元素的底層數組 
 * 至於 transient 關鍵字這裏簡單說一句,被它修飾的成員變量沒法被 Serializable 序列化 
 * 有興趣的能夠去網上查相關資料
 */
transient Object[] elementData; // non-private to simplify nested class access
複製代碼

對於上述幾個成員變量,咱們只是在註釋中簡單的說明,對於他們具體有什麼做用,在下邊分析構造方法和擴容機制的時候將會更詳細的講解。多線程

ArrayList 一共三種構造方式,咱們先從無參的構造方法來開始:

無參構造方法

/**
 * 構造一個初始容量爲10的空列表。
 */
public ArrayList() {
   this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
複製代碼

這是咱們常用的一個構造方法,其內部實現只是將 elementData 指向了咱們剛纔講得 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 這個空數組,這個空數組的容量是 0, 可是源碼註釋卻說這是構造一個初始容量爲10的空列表。這是爲何?其實在集合調用 add 方法添加元素的時候將會調用 ensureCapacityInternal 方法,在這個方法內部判斷了:

if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
       minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
複製代碼

可見,若是採用無參數構造方法的時候第一次添加元素確定走進 if 判斷中 minCapacity 將被賦值爲 10,因此「構造一個初始容量爲10的空列表。」也就是這個意思。

指定初始容量的構造方法

/**
 * 構造一個具備指定初始容量的空列表。
 * @param  初始容量 
 * @throws 若是參數小於 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);
   }
}
複製代碼

若是咱們預先知道一個集合元素的容納的個數的時候推薦使用這個構造方法,好比咱們有個一 FragmentPagerAdapter 一共須要裝 15 個 Fragment ,那麼咱們就能夠在構造集合的時候生成一個初始容量爲 15 的一個集合。有人會認爲 ArrayList 自身具備動態擴容的機制,無需這麼麻煩,下面咱們講解擴容機制的時候咱們就會發現,每次擴容是須要有必定的內存開銷的,而這個開銷在預先知道容量的時候是能夠避免的。

源代碼中指定初始容量的構造方法實現,判斷了若是 咱們指定容量大於 0 ,將會直接 new 一個數組,賦值給 elementData 引用做爲集合真正的存儲數組,而指定容量等於 0 的時候講使用成員變量 EMPTY_ELEMENTDATA 做爲暫時的存儲數組,這是 EMPTY_ELEMENTDATA 這個空數組的一個用處(沒必要太過於糾結 EMPTY_ELEMENTDATA 的做用,其實它的在源碼中出現的頻率並不高)。

使用另個一個集合 Collection 的構造方法

/**
 * 構造一個包含指定集合元素的列表,元素的順序由集合的迭代器返回。
 *
 * @param 源集合,其元素將被放置到這個集合中。 
 * @若是參數爲 null,將會拋出 NullPointerException 空指針異常
 */
 public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray 可能(錯誤地)不返回 Object[]類型的數組 參見 jdk 的 bug 列表(6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 若是集合大小爲空將賦值爲 EMPTY_ELEMENTDATA 等同於 new ArrayList(0);
        this.elementData = EMPTY_ELEMENTDATA;
    }
}
複製代碼

看完這個代碼我最疑惑的地方是 Collection.toArray()Arrays.copyOf() 這兩個方法的使用,看來想明白這個構造參數具體作了什麼必須理解這兩個方法了。

Object[] Collection.toArray() 方法

咱們都知道 Collection 是集合框架的超類,其實 Collection.toArray 是交給具體的集合子類去實現的,這就說明不一樣的集合可能有不一樣的實現。他用來將一個集合轉化爲一個 Object[] 數組,事實上的真的是這樣的麼?參見 jdk 的 bug 列表(6260652)又是什麼意思呢 ?咱們來看下下邊的這個例子:

List<String> subClasses = Arrays.asList("abc","def");

// class java.util.Arrays$ArrayList  
System.out.println(list.getClass());  
    
Object[] objects = subClasses.toArray();

// class java.lang.String;  
Object[] objArray = list.toArray();  
//這裏返回的是 String[]
System.out.println(objects.getClass().getSimpleName()); 

objArray[0] = new Object(); // cause ArrayStoreException  
複製代碼

咦?爲啥這裏並非一個 Object 數組呢?其實咱們注意到,list.getClass 獲得的並非咱們使用的 ArrayList 而是 Arrays 的內部類 Arrays$ArrayList

ArrayList(E[] array) {
       //這裏只是檢查了數組是否爲空,不爲空直接將原數組賦值給這個 ArrayList 的存儲數組。
       a = Objects.requireNonNull(array);
}

@Override
public Object[] toArray(){
  return a.clone();
}

複製代碼

而咱們調用的 toArray 方法就是這個內部對於 Collection.toArray 的實現,a.clone() ,這裏 clone 並不會改變一個數組的類型,因此當原始數組中放的 String 類型的時候就會出現上邊的這種狀況了。

其實咱們能夠認爲這是 jdk 的一個 bug,早在 05年的時候被人提出來了,可是一直沒修復,可是在新的 「jdk 1.9」 種這個 bug 被修復了。

有興趣的能夠追蹤 bug 6260652 看下。

Arrays.copyOf 方法

這個方法是在集合源碼中常見的一個方法,他有不少重載方式,咱們來看下最根本的方法:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
   @SuppressWarnings("unchecked")
    //根據class的類型是不是 Object[] 來決定是 new 仍是反射去構造一個泛型數組
   T[] copy = ((Object)newType == (Object)Object[].class)
       ? (T[]) new Object[newLength]
       : (T[]) Array.newInstance(newType.getComponentType(), newLength);
       //使用 native 方法批量賦值元素至新數組中。
   System.arraycopy(original, 0, copy, 0,
                    Math.min(original.length, newLength));
   return copy;
}
複製代碼

上邊的註釋也看出來了,Arrays.copyOf 方法複製數組的時候先判斷了指定的數組類型是否爲 Object[] 類型,不然使用反射去構造一個指定類型的數組。最後使用 System.arraycopy這個 native 方法,去實現最終的數組賦值,newLength 若是比 original.length 大的時候會將多餘的空間賦值爲 null 由下邊的例子可見:

String[] arrString = {"abc","def"};

Object[] copyOf = Arrays.copyOf(arrString, 5, Object[].class);
//[abc, def, null, null, null]
System.out.println(Arrays.toString(copyOf));
複製代碼

固然 ArrayList(Collection<? extends E> c) 複製的時候傳遞的是 c.size() 因此不會出現 null

ex: 對於 System.arraycopy 該方法,本文再也不展開討論,有一篇對於其分析很好的文章你們能夠去參考System:System.arraycopy方法詳解

ok,繞了這麼大的圈子終於明白了,ArrayList(Collection<? extends E> c)幹了啥了,其實就是將一個集合中的元素塞到 ArrayList 底層的數組中。至此咱們也將 ArrayList 的構造研究完了。

ArrayList的添加元素 & 擴容機制

敲黑板了!這塊是面試的常客了,因此必須仔細研究下了。咱們先看下如何給一個 ArrayList 添加一個元素:

在集合末尾添加一個元素的方法

//成員變量 size 標識集合當前元素個數初始爲 0
int size;
/**
 * 將指定元素添加到集合(底層數組)末尾
 * @param 將要添加的元素
 * @return 返回 true 表示添加成功
 */
 public boolean add(E e) {
    //檢查當前底層數組容量,若是容量不夠則進行擴容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //將數組添加一個元素,size 加 1
    elementData[size++] = e;
    return true;
 }
複製代碼

調用 add 方法的時候總會調用 ensureCapacityInternal 來判斷是否須要進行數組擴容,ensureCapacityInternal 參數爲當前集合長度 size + 1,這很好理解,是否須要擴充長度,須要看當前底層數組是否夠放 size + 1 個元素的。

擴容機制

//擴容檢查
private void ensureCapacityInternal(int minCapacity) {
    //若是是無參構造方法構造的的集合,第一次添加元素的時候會知足這個條件 minCapacity 將會被賦值爲 10
   if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
       minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
   }
    // 將 size + 1 或 10 傳入 ensureExplicitCapacity 進行擴容判斷
   ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
  //操做數加 1 用於保證併發訪問 
   modCount++;
   // 若是 當前數組的長度比添加元素後的長度要小則進行擴容 
   if (minCapacity - elementData.length > 0)
       grow(minCapacity);
}
複製代碼

上邊的源碼主要作了擴容前的判斷操做,注意參數爲當前集合元素個數+1,第一次添加元素的時候 size + 1 = 1 ,而 elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA, 長度爲 0 ,1 - 0 > 0, 因此須要進行 grow 操做也就是擴容。

/**
 * 集合的最大長度 Integer.MAX_VALUE - 8 是爲了減小出錯的概率 Integer 最大值已經很大了
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * 增長容量,以確保它至少能容納最小容量參數指定的元素個數。
 * @param 知足條件的最小容量
 */
private void grow(int minCapacity) {
  //獲取當前 elementData 的大小,也就是 List 中當前的容量
   int oldCapacity = elementData.length;
   //oldCapacity >> 1 等價於 oldCapacity / 2  因此新容量爲當前容量的 1.5 倍
   int newCapacity = oldCapacity + (oldCapacity >> 1);
   //若是擴大1.5倍後仍舊比 minCapacity 小那麼直接等於 minCapacity
   if (newCapacity - minCapacity < 0)
       newCapacity = minCapacity;
    //若是新數組大小比  MAX_ARRAY_SIZE 就須要進一步比較 minCapacity 和 MAX_ARRAY_SIZE 的大小
   if (newCapacity - MAX_ARRAY_SIZE > 0)
       newCapacity = hugeCapacity(minCapacity);
   // minCapacity一般接近 size 大小
   //使用 Arrays.copyOf 構建一個長度爲 newCapacity 新數組 並將 elementData 指向新數組
   elementData = Arrays.copyOf(elementData, newCapacity);
}

/**
 * 比較 minCapacity 與 Integer.MAX_VALUE - 8 的大小若是大則放棄-8的設定,設置爲 Integer.MAX_VALUE 
 */
private static int hugeCapacity(int minCapacity) {
   if (minCapacity < 0) // overflow
       throw new OutOfMemoryError();
   return (minCapacity > MAX_ARRAY_SIZE) ?
       Integer.MAX_VALUE :
       MAX_ARRAY_SIZE;
}
複製代碼

由此看來 ArrayList 的擴容機制的知識點一共又兩個

  1. 每次擴容的大小爲原來大小的 1.5倍 (固然這裏沒有包含 1.5倍後大於 MAX_ARRAY_SIZE 的狀況)
  2. 擴容的過程實際上是一個將原來元素拷貝到一個擴容後數組大小的長度新數組中。因此 ArrayList 的擴容實際上是相對來講比較消耗性能的。

在指定角標位置添加元素的方法

/**
* 將指定的元素插入該列表中的指定位置。將當前位置的元素(若是有)和任何後續元素移到右邊(將一個元素添加到它們的索引中)。
* 
* @param 要插入的索引位置
* @param 要添加的元素
* @throws 若是 index 大於集合長度 小於 0 則拋出角標越界 IndexOutOfBoundsException 異常
*/
public void add(int index, E element) {
   // 檢查角標是否越界
   rangeCheckForAdd(index);
    // 擴容檢查
   ensureCapacityInternal(size + 1);      
   //調用 native 方法新型數組拷貝
   System.arraycopy(elementData, index, elementData, 
                    index + 1,size - index);
    // 添加新元素
   elementData[index] = element;
   size++;
}
複製代碼

咱們知道一個數組是不能在角標位置直接插入元素的,ArrayList 經過數組拷貝的方法將指定角標位置以及其後續元素總體向後移動一個位置,空出 index 角標的位置,來賦值新的元素。

將一個數組 src 起始 srcPos 角標以後 length 長度間的元素,賦值到 dest 數組中 destPosdestPos + length -1長度角標位置上。只是在 add 方法中 srcdestPos 爲同一個數組而已。

public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);
複製代碼

批量添加元素

因爲批量添加和添加一個元素邏輯大概相同則這裏不詳細說了,代碼註釋能夠了解整個添加流程。

在數組末尾添加

public boolean addAll(Collection<? extends E> c) {
        // 調用 c.toArray 將集合轉化數組
        Object[] a = c.toArray();
        // 要添加的元素的個數
        int numNew = a.length;
        //擴容檢查以及擴容
        ensureCapacityInternal(size + numNew);  // Increments modCount
        //將參數集合中的元素添加到原來數組 [size,size + numNew -1] 的角標位置上。
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        //與單一添加的 add 方法不一樣的是批量添加有返回值,若是 numNew == 0 表示沒有要添加的元素則須要返回 false 
        return numNew != 0;
}
複製代碼

在數組指定角標位置添加

public boolean addAll(int index, Collection<? extends E> c) {
        //一樣檢查要插入的位置是否會致使角標越界
        rangeCheckForAdd(index);
        
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew); 
        //這裏作了判斷,若是要numMoved > 0 表明插入的位置在集合中間位置,和在 numMoved == 0最後位置 則表示要在數組末尾添加 若是 < 0  rangeCheckForAdd 就跑出了角標越界
        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;
    }
    
private void rangeCheckForAdd(int index) {
   if (index > size || index < 0)
       throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

複製代碼

兩個方法不一樣的地方在於若是移動角標即以後的元素,addAll(int index, Collection<? extends E> c)裏作了判斷,若是要 numMoved > 0 表明插入的位置在集合中間位置,和在 numMoved == 0 最後位置 則表示要在數組末尾添加 若是 numMoved < 0rangeCheckForAdd 就拋出了角標越界異常了。

與單一添加的 add 方法不一樣的是批量添加有返回值,若是 numNew == 0 表示沒有要添加的元素則須要返回 false

ArrayList 刪除元素

根據角標移除元素

/**
* 將任何後續元素移到左邊(從它們的索引中減去一個)。
*/
public E remove(int index) {
   //檢查 index 是否 >= size
   rangeCheck(index);

   modCount++;
   //index 位置的元素 
   E oldValue = elementData(index);
    // 須要移動的元素個數
   int numMoved = size - index - 1;
   if (numMoved > 0)
        //採用拷貝賦值的方法將 index 以後全部的元素 向前移動一個位置
       System.arraycopy(elementData, index+1, elementData, index,
                        numMoved);
   // 將 element 末尾的元素位置設爲 null                 
   elementData[--size] = null; // clear to let GC do its work
    // 返回 index 位置的元素 
   return oldValue;
}

// 比較要移除的角標位置和當前 elementData 中元素的個數
private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
複製代碼

根絕角標移除元素的方法源碼如上所示,值得注意的地方是:

rangeCheckrangeCheckForAdd 方法不一樣 ,rangeCheck 只檢查了 index是否大於等於 size,由於咱們知道 sizeelementData 已存儲數據的個數,咱們只能移除 elementData 數組中 [0 , size -1] 的元素,不然應該拋出角標越界。

可是爲何沒有 和 rangeCheckForAdd 同樣檢查小於0的角標呢,是否是remove(-1) 不會拋異常呢? 其實不是的,由於 rangeCheck(index); 後咱們去調用 elementData(index) 的時候也會拋出 IndexOutOfBoundsException 的異常,這是數組自己拋出的,不是 ArrayList 拋出的。那爲何要檢查>= size 呢? 數組自己不也會檢查麼? 哈哈.. 細心的同窗確定知道 elementData.length 並不必定等於 size,好比:

ArrayList<String> testRemove = new ArrayList<>(10);

   testRemove.add("1");
   testRemove.add("2");
    // java.lang.IndexOutOfBoundsException: Index: 2, Size: 2
   String remove = testRemove.remove(2);
    
   System.out.println("remove = " + remove + "");
複製代碼

new ArrayList<>(10) 表示 elementData 初始容量爲10,因此elementData.length = 10 而咱們只給集合添加了兩個元素因此 size = 2 這也就是爲啥要 rangeCheck 的緣由了。

移除指定元素

/**
* 刪除指定元素,若是它存在則反會 true,若是不存在返回 false。
* 更準確地說是刪除集合中第一齣現 o 元素位置的元素 ,
* 也就是說只會刪除一個,而且若是有重複的話,只會刪除第一個次出現的位置。
*/
public boolean remove(Object o) {
    // 若是元素爲空則只需判斷 == 也就是內存地址
   if (o == null) {
       for (int index = 0; index < size; index++)
           if (elementData[index] == null) {
                //獲得第一個等於 null 的元素角標並移除該元素 返回 ture
               fastRemove(index);
               return true;
           }
   } else {
        // 若是元素不爲空則須要用 equals 判斷。
       for (int index = 0; index < size; index++)
           if (o.equals(elementData[index])) {
                //獲得第一個等於 o 的元素角標並移除該元素 返回 ture
               fastRemove(index);
               return true;
           }
   }
   return false;
}

//移除元素的邏輯和 remve(Index)同樣 
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
}

複製代碼

由上邊代碼能夠看出來,移除元素和移除指定角標元素同樣最終都是 經過 System.arraycopy 將 index 以後的元素前移一位,並釋放原來位於 size 位置的元素。

還能夠看出,若是數組中有指定多個與 o 相同的元素只會移除角標最小的那個,而且 null 和 非null 的時候判斷方法不同。至於 equals 和 == 的區別,還有 hashCode 方法,我會以後在總結一篇單獨的文章。等不急的能夠先去網上找找嘍。

批量移除/保留 removeAll/retainAll

ArrayList 提供了 removeAll/retainAll 操做,這兩個操做分別是 批量刪除與參數集合中共同享有的元素 和 批量刪除與參數集合中不共同享有的元素,保留共同享有的元素,因爲兩個方法只有一個參數不一樣

/** 批量刪除與參數集合中共同享有的元素*/
 public boolean removeAll(Collection<?> c) {
        //判空 若是爲空則拋出 NullPointerException 異常 Objects 的方法
        Objects.requireNonNull(c);
        return batchRemove(c, false);
 }
 
 /** 只保留與 c 中元素相同的元素相同的元素*/
public boolean retainAll(Collection<?> c) {
   Objects.requireNonNull(c);
   return batchRemove(c, true);
}
 
 /** 批量刪除的指定方法 */
private boolean batchRemove(Collection<?> c, boolean complement) {
   
   final Object[] elementData = this.elementData;
    // r w 兩個角標 r 爲 elementData 中元素的索引 
    // w 爲刪除元素後集合的長度 
   int r = 0, w = 0;
   boolean modified = false;
   try {
       for (; r < size; r++)
            // 若是 c 當前集合中不包含當前元素,那麼則保留
           if (c.contains(elementData[r]) == complement)
               elementData[w++] = elementData[r];
   } finally {
       // 若是c.contains(o)可能會拋出異常,若是拋出異常後 r!=size 則將 r 以後的元素不在比較直接放入數組
       if (r != size) {
           System.arraycopy(elementData, r,
                            elementData, w,
                            size - r);
          // w 加上剩餘元素的長度
           w += size - r;
       }
        // 若是集合移除過元素,則須要將 w 以後的元素設置爲 null 釋放內存
       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;
}
複製代碼

能夠看到移除指定集合中包含的元素的方法代碼量是目前分析代碼中最長的了,可是邏輯也很清晰:

  1. 從 0 開始遍歷 elementData 若是 r 位置的元素不存在於指定集合 c 中,那麼咱們就將他複製給數組 w 位置, 整個遍歷過程當中 w <= r
  2. 因爲 c.contains(o)可能會拋出異常 ClassCastException/NullPointerException,若是由於異常而終止(這兩個異常是可選操做,集合源碼中並無顯示生命該方法必定會拋異常),那麼咱們將會產生一次錯誤操做,因此 finally 中執行了判斷操做,若是 r!= size 那麼確定是發生了異常,那麼則將 r 以後的元素不在比較直接放入數組。最終獲得的結果並不必定正確是刪除了全部與 c 中的元素。
  3. 批量刪除和保存中,涉及高效的保存/刪除兩個集合公有元素的算法,是值得咱們學習的地方。

ArraList 的改查

對於一個ArrayList 的改查方法就很簡單了,set 和 get 方法。下面咱們看下源碼吧:

修改指定角標位置的元素

public E set(int index, E element) {
    //角標越界檢查
   rangeCheck(index);
 //下標取數據注意這裏不是elementData[index] 而是 elementData(index) 方法
   E oldValue = elementData(index);
   //將 index 位置設置爲新的元素
   elementData[index] = element;
   // 返回以前在 index 位置的元素
   return oldValue;
}

E elementData(int index) {
    return (E) elementData[index];
}

複製代碼

查詢指定角標的元素

public E get(int index) {
    //越界檢查
    rangeCheck(index);
    //下標取數據注意這裏不是elementData[index] 而是 elementData(index) 方法
    return elementData(index); 
}

複製代碼

查詢指定元素的角標或者集合是否包含某個元素

//集合中是否包含元素 indexOf 返回 -1 表示不包含 return false 不然返回 true
public boolean contains(Object o) {
   return indexOf(o) >= 0;
}

/**
* 返回集合中第一個與 o 元素相等的元素角標,返回 -1 表示集合中不存在這個元素
* 這裏還作了空元素直接判斷 == 的操做
*/
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;
}
    
 /** 
  * 從 elementData 末尾開始遍歷遍歷數組,因此返回的是集合中最後一個與 o 相等的元素的角標
  */
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;
}

複製代碼

ArrayList 集合的 toArry 方法

其實 Object[] toArray(); 方法,以及其重載函數 <T> T[] toArray(T[] a); 是接口 Collection 的方法,ArrayList 實現了這兩個方法,不多見ArrayList 源碼分析的文章分析這兩個方法,顧名思義這兩個方法的是用來,將一個集合轉爲數組的方法,那麼二者的不一樣之處是,後者能夠指定數組的類型,前者返回爲一個 Object[] 超類數組。那麼咱們具體下源碼實現:

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

@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
   if (a.length < size)
       // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; } 複製代碼

能夠看到 Object[] toArray() 只是調用了一次 Arrays.copyOf 將集合中元素拷貝到一個新的 Object[] 數組並返回。這個 Arrays.copyOf 方法前邊已經講了。因此 toArray 方法並無什麼疑問,有疑問的地方在於toArray(T[] a)

咱們能夠傳入一個指定類型的標誌數組做爲參數,toArray(T[] a) 方法最終會返回這個類型的包含集合元素的新數組。可是源碼判斷了 :

  1. 若是 a.length < size 即當前集合元素的個數與參數 a 數組元素的大小的時候將和 toArray() 同樣返回一個新的數組。

  2. 若是 a.length == size 將不會產生新的數組直接將集合中的元素調用 System.arraycopy 方法將元素複製到參數數組中,返回 a。

  3. a.length > size 也不會產生新的數組,可是值得注意的是 a[size] = null; 這一句改變了原數組中 index = size 位置的元素,被從新設置爲 null 了。

下面咱們來看下第三種狀況的例子:

SubClass[] sourceMore = new SubClass[4];
   for (int i = 0; i < sourceMore.length; i++) {
       sourceMore[i] = new SubClass(i);
}
        
//當 List.toArray(T[] a) 中 a.length == list.size 的時候使用 Array.copyOf 會將 list 中的內容賦值給 sourceMore 並將其返回
//sourceMore[0,size-1] = list{0, size-1} 而 sourceMore[size] = null

SubClass[] sourceMore = new SubClass[4];
for (int i = 0; i < sourceMore.length; i++) {
  sourceMore[i] = new SubClass(i);
}

//list to Array 以前 sourceMore [SubClass{test=0}, SubClass{test=1}, SubClass{test=2}, SubClass{test=3}]   sourceEqual.length:: 4
System.out.println("list to Array 以前 sourceMore " + Arrays.toString(sourceMore) + " sourceEqual.length:: " + sourceMore.length);

SubClass[] desSourceMore = tLists.toArray(sourceMore);
//list to Array 以後 desSourceMore [SubClass{test=1}, SubClass{test=2}, null, SubClass{test=3}]desSourceMore.length:: 4
System.out.println("list to Array 以後 desSourceMore " + Arrays.toString(desSourceMore) + "desSourceMore.length:: " + desSourceMore.length);

//list to Array 以後 source [SubClass{test=1}, SubClass{test=2}, null, SubClass{test=3}]sourceEqual.length:: 4
System.out.println("list to Array 以後 source " + Arrays.toString(sourceMore) + "sourceEqual.length:: " + sourceMore.length);

//source ==  desSource true
System.out.println("source == desSource " + (sourceMore == desSourceMore));

複製代碼

ArrayList 的遍歷

ArrayList 的遍歷方式 jdk 1.8 以前有三種 :for 循環遍歷, foreach 遍歷,迭代器遍歷,jdk 1.8 以後又引入了forEach 操做,咱們先來看看迭代器的源碼實現:

迭代器

迭代器 Iterator 模式是用於遍歷各類集合類的標準訪問方法。它能夠把訪問邏輯從不一樣類型的集合類中抽象出來,從而避免向客戶端暴露集合的內部結構。 ArrayList 做爲集合類也不例外,迭代器自己只提供三個接口方法:

public interface Iterator {
        boolean hasNext();//是否還有下一個元素
        Object next();// 返回當前元素 能夠理解爲他至關於 fori 中 i 索引
        void remove();// 移除一個當前的元素 也就是 next 元素。
    }
複製代碼

ArrayList 中調用 iterator() 將會返回一個內部類對象 Itr 其實現了 Iterator 接口。

public Iterator<E> iterator() {
        return new Itr();
}
複製代碼

下面讓咱們看下其實現的源碼:

正如咱們的 for 循環遍歷同樣,數組角標老是從 0 開始的,因此 cursor 初始值爲 0 , hasNext 表示是否遍歷到數組末尾,即 i < size 。對於 modCount 變量之因此一直沒有介紹是由於他集合併發訪問有關係,用於標記當前集合被修改(增刪)的次數,若是併發訪問了集合那麼將會致使這個 modCount 的變化,在遍歷過程當中不正確的操做集合將會拋出 ConcurrentModificationException ,這是 Java 「fast-fail 的機制」,對於若是正確的在遍歷過程當中操做集合稍後會有說明。

private class Itr implements Iterator<E> {
   int cursor; // 對照 hasNext 方法 cursor 應理解爲下個調用 next 返回的元素 初始爲 0
   int lastRet = -1; // 上一個返回的角標
   int expectedModCount = modCount;//初始化的時候將其賦值爲當前集合中的操做數,
   // 是否還有下一個元素 cursor == size 表示當前集合已經遍歷完了 因此只有當 cursor 不等於 size 的時候 纔會有下一個元素
   public boolean hasNext() {
       return cursor != size;
   }

複製代碼

next 方法是咱們獲取集合中元素的方法,next 返回當前遍歷位置的元素,若是在調用 next 以前集合被修改,而且迭代器中的指望操做數並無改變,將會引起ConcurrentModificationException。next 方法屢次調用 checkForComodification 來檢驗這個條件是否成立。

@SuppressWarnings("unchecked")
   public E next() {
        // 驗證指望的操做數與當前集合中的操做數是否相同 若是不一樣將會拋出異常
       checkForComodification();
       // 若是迭代器的索引已經大於集合中元素的個數則拋出異常,這裏不拋出角標越界
       int i = cursor;
       if (i >= size)
           throw new NoSuchElementException();
           
       Object[] elementData = ArrayList.this.elementData;
       // 因爲多線程的問題這裏再次判斷是否越界,若是有異步線程修改了List(增刪)這裏就可能產生異常
       if (i >= elementData.length)
           throw new ConcurrentModificationException();
       // cursor 移動
       cursor = i + 1;
       //最終返回 集合中對應位置的元素,並將 lastRet 賦值爲已經訪問的元素的下標
       return (E) elementData[lastRet = i];
   }

複製代碼

只有 Iteratorremove 方法會在調用集合的 remove 以後讓 指望 操做數改變使expectedModCountmodCount 再相等,因此是安全的。

// 實質調用了集合的 remove 方法移除元素
   public void remove() {
        // 好比操做者沒有調用 next 方法就調用了 remove 操做,lastRet 等於 -1的時候拋異常
       if (lastRet < 0)
           throw new IllegalStateException();
           
        //檢查操做數
       checkForComodification();
    
       try {
            //移除上次調用 next 訪問的元素
           ArrayList.this.remove(lastRet);
           // 集合中少了一個元素,因此 cursor 向前移動一個位置(調用 next 時候 cursor = lastRet + 1)
           cursor = lastRet;
           //刪除元素後賦值-1,確保先前 remove 時候的判斷
           lastRet = -1;
           //修改操做數指望值, modCount 在調用集合的 remove 的時候被修改過了。
           expectedModCount = modCount;
       } catch (IndexOutOfBoundsException ex) {
            // 集合的 remove 會有可能拋出 rangeCheck 異常,catch 掉統一拋出 ConcurrentModificationException 
           throw new ConcurrentModificationException();
       }
   }

複製代碼

檢查指望的操做數與當前集合的操做數是否相同。Java8 發佈了不少函數式編程的特性包括 lamadaStream 操做。迭代器也所以添加了 forEachRemaining 方法,這個方法能夠將當前迭代器訪問的元素(next 方法)後的元素傳遞出去還沒用到過,源碼就不放出來了,你們有興趣本身瞭解下。

@Override
   @SuppressWarnings("unchecked")
   public void forEachRemaining(Consumer<? super E> consumer) {
     ... Java8 的新特性,能夠將當前迭代器訪問的元素(next 方法)後的元素傳遞出去還沒用到過,源碼就不放出來了,你們有興趣本身瞭解下。
   }
    // 檢查指望的操做數與當前集合的操做數是否相同
   final void checkForComodification() {
       if (modCount != expectedModCount)
           throw new ConcurrentModificationException();
   }
}
複製代碼

ListIterator 迭代器

ArrayList 能夠經過如下兩種方式獲取 ListIterator 迭代器,區別在於初始角標的位置。不帶參數的迭代器默認的cursor = 0

public ListIterator<E> listIterator(int index) {
   if (index < 0 || index > size)
       throw new IndexOutOfBoundsException("Index: "+index);
   return new ListItr(index);
}
    
public ListIterator<E> listIterator() {
   return new ListItr(0);
}
複製代碼

ListItr對象繼承自前邊分析的 Itr,也就是說他擁有 Itr 的全部方法,並在此基礎上進行擴展,其擴展了訪問當前角標前一個元素的方法。以及在遍歷過程當中添加元素和修改元素的方法。

ListItr 的構造方法以下:

private class ListItr extends Itr implements ListIterator<E> {
   ListItr(int index) {
       super();
       cursor = index;
}
複製代碼

ListItrprevious 方法:

public boolean hasPrevious() {
 // cursor = 0 表示遊標在數組第一個元素的左邊,此時 `hasPrevious` 返回false
  return cursor != 0;
}

public int nextIndex() {
  return cursor;//調用返回當前角標位置
}

public int previousIndex() {
  return cursor - 1;//調用返回上一個角標
}

//返回當前角標的上一個元素,並前移移動角標
@SuppressWarnings("unchecked")
public E previous() {
  // fast-fail 檢查
  checkForComodification();
  int i = cursor - 1;
  // 若是前移角標 <0 表明遍歷到數組遍歷完成,通常在調用 previous 要調用 hasPrevious 判斷
  if (i < 0)
      throw new NoSuchElementException();
  //獲取元素    
  Object[] elementData = ArrayList.this.elementData;
  if (i >= elementData.length)
      throw new ConcurrentModificationException();
  //獲取成功後修改角標位置和 lastRet 位置    
  cursor = i;
  return (E) elementData[lastRet = i];
}
複製代碼

ListItradd 方法

public void add(E e) {
  // fast-fail 檢查
  checkForComodification();
  try {
      // 獲取當前角標位置,通常的是調用 previous 後,角標改變後後去 cursor 
      int i = cursor;
      //添加元素在角標位置
      ArrayList.this.add(i, e);
      //集合修改完成後要改變當前角標位置
      cursor = i + 1;
      //從新置位 -1 若是使用迭代器修改了角標位置元素後不容許馬上使用 set 方法修改修改後角標未知的額元素 參考 set 的源代碼
      lastRet = -1;
      expectedModCount = modCount;
  } catch (IndexOutOfBoundsException ex) {
      throw new ConcurrentModificationException();
  }
}
複製代碼

可能對比兩個迭代器後,會對 curor 指向的位置有所疑惑,如今咱們來看下一段示例代碼對應的圖:

private void testListItr(){
   ArrayList<Integer> list  = new ArrayList<>();
   list.add(1);
   list.add(2);
   list.add(3);
   list.add(4);

   ListIterator<Integer> listIterator = list.listIterator(list.size());

   while (listIterator.hasPrevious()){

       if (listIterator.previous() == 2){
           listIterator.add(0); 
//         listIterator.set(10); //Exception in thread "main" java.lang.IllegalStateException
       }

   }

   System.out.println("list " + list.toString());

}
複製代碼

由此能夠看 cursor 於 數組角標不一樣,它能夠處的位置總比角標多一個,由於在咱們使用 Iterator 操做集合的時候,老是要先操做 cursor 移動, listIterator.previous 也好 iterator.next() 也好,都是同樣的道理,若是不按照規定去進行操做,帶給使用者的只有異常。

java8 新增長的遍歷方法 forEach

java8增長不少好用的 API,工做和學習中也在慢慢接觸這些 API,forEach 操做多是我繼 lambda 後,第一個使用的 API 了(囧),jdk doc 對這個方法的解釋是:

對此集合的每一個條目執行給定操做,直處處理完全部條目或操做拋出異常爲止。 除非實現類另有規定,不然按照條目集迭代的順序執行操做(若是指定了迭代順序)。操做拋出的異常須要調用者本身處理。

其實其內部實現也很簡單,只是一個判斷了操做數的 for 循環,因此在效率上不會有提高,可是在安全性上的確有提高,也少些不少代碼不是麼?

@Override
public void forEach(Consumer<? super E> action) {
    //檢查調用者傳進來的操做函數是否爲空
   Objects.requireNonNull(action);
   //與迭代不一樣指望操做被賦值爲 final 也就是 forEach 過程當中不容許併發修改集合不然會拋出異常
   final int expectedModCount = modCount;
   @SuppressWarnings("unchecked")
   final E[] elementData = (E[]) this.elementData;
   final int size = this.size;
   //每次取元素以前判斷操做數,確保操做正常
   for (int i=0; modCount == expectedModCount && i < size; i++) {
       action.accept(elementData[i]);
   }
   if (modCount != expectedModCount) {
       throw new ConcurrentModificationException();
   }
}

複製代碼

對於高級 for 循環以及最普通的 fori 方法這裏再也不贅述。下面咱們看下面試會問到一個問題,也是咱們在單線程操做集合的時候須要注意的一個問題,若是正確的在遍歷過程當中修改集合。

錯誤操做 1 在 for循環修改集合後繼續遍歷

第一個例子:

List<SubClass> list2 = new ArrayList<>();

list2.add(new SubClass(1));
list2.add(new SubClass(2));
list2.add(new SubClass(3));
list2.add(new SubClass(3));

for (int i = 0; i < list2.size(); i++) {
  if (list2.get(i).test == 3) {
      list2.remove(i);
  }
}
System.out.println(list2);
//[SubClass{test=1}, SubClass{test=2}, SubClass{test=3}]

複製代碼

這個例子咱們會發現,程序並無拋出異常,可是從運行通過上來看並非咱們想要的,由於還有 SubClass.test = 3的數據在,這是由於 remove 操做改變了list.size(),而 fori 中每次執行都會從新調用一次lists2.size(),當咱們刪除了倒數第二個元素後,list2.size() = 3,i = 3 < 3 不成立則沒有在進行 remove 操做,知道了爲何之後咱們試着這樣改變了循環方式:

int size = list2.size();
for (int i = 0; i < size; i++) {
  if (list2.get(i).test == 3) {
      list2.remove(i);//remove 之後 list 內部將 size 從新改變了 for 循環下次調用的時候可能就不進去了
  }
}
System.out.println(list2);

//Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 3, Size: 3
複製代碼

果然程序拋出了角標越界的異常,由於這樣每次 fori 的時候咱們不去拿更新後的 list 元素的 size 大小,因此當咱們刪除一個元素後,size = 3 當咱們 for 循環去list2.get(3)的時候就會被 rangeCheck方法拋出異常。

錯誤操做致使 ConcurrentModificationException 異常

咱們分析迭代器的時候,知道 ConcurrentModificationException是指由於迭代器調用 checkForComodification 方法比較 modCountexpectedModCount 方法大小的時候拋出異常。咱們在分析 ArrayList 的時候在每次對集合進行修改, 即有 add 和 remove 操做的時候每次都會對 modCount ++

modCount 這個變量主要用來記錄 ArrayList 被修改的次數,那麼爲何要記錄這個次數呢?是爲了防止多線程對同一集合進行修改產生錯誤,記錄了這個變量,在對 ArrayList 進行迭代的過程當中咱們能很快的發現這個變量是否被修改過,若是被修改了 ConcurrentModificationException 將會產生。下面咱們來看下例子,這個例子並非在多線程下的,而是由於咱們在同一線程中對 list 進行了錯誤操做致使的:

Iterator<SubClass> iterator = lists.iterator();

while (iterator.hasNext()) {
  SubClass next = iterator.next();
  int index = next.test;
  if (index == 3) {
      list2.remove(index);//操做1: 注意是 list2.remove 操做
      //iterator.remove();/操做2 注意是 iterator.remove 操做
  }
}
//操做1: Exception in thread "main" java.util.ConcurrentModificationException
//操做2:  [SubClass{test=1}, SubClass{test=2}]
System.out.println(list2);
複製代碼

咱們對操做1,2分別運行程序,能夠看到,操做1很快就拋出了 java.util.ConcurrentModificationException 異常,操做2 則順利運行出正常結果,若是對 modCount 注意了的話,咱們很容易理解,list.remove(index) 操做會修改ListmodCount,而 iterator.next() 內部每次會檢驗 expectedModCount != modCount,因此當咱們使用 list.remove 下一次再調用 iterator.next() 就會報錯了,而iterator.remove爲何是安全的呢?由於其操做內部會在調用 list.remove 後從新將新的 modCount 賦值給 expectedModCount。因此咱們直接調用 list.remove 操做是錯誤的。對於多線程的影響這裏不在展開這裏推薦有興趣的朋友看下這個文章 Java ConcurrentModificationException異常緣由和解決方法;

通過了一輪分析咱們咱們知道了錯誤產生緣由了,可是你們是否能真的分辨出什麼操做是錯誤的呢?咱們來看下邊這個面試題,這是我在網上無心中看到的一道大衆點評的面試題:

ArrayList<String> list = new ArrayList<String>();
for (int i = 0; i < 10; i++) {
  list.add("sh" + i);
}

for (int i = 0; list.iterator().hasNext(); i++) {
  list.remove(i);
  System.out.println("祕密" + list.get(i));
}
複製代碼

一道面試題

相信你們確定知道這樣操做是會產生錯誤的,可是最終會拋出角標越界仍是ConcurrentModificationException呢?

其實這裏會拋出角標越界異常,爲何呢,由於 for 循環的條件 list.iterator().hasNext(),咱們知道 list.iterator() 將會new 一個新的 iterator 對象,而在 new 的過程當中咱們將 每次 list.remove 後的 modCount 賦值給了新的 iteratorexpectedModCount,因此不會拋出 ConcurrentModificationException 異常,而 hasNext 內部只判斷了 size 是否等於 cursor != size 當咱們刪除了一半元素之後,size 變成了 5 而新的 list.iterator() 的 cursor 等於 0 ,0!=5 for 循環繼續,那麼當執行到 list.remove(5)的時候就會拋出角標越界了。

總結

  1. ArrayList 底層是一個動態擴容的數組結構,每次擴容須要增長1.5倍的容量
  2. ArrayList 擴容底層是經過 Arrays.CopyOfSystem.arraycopy 來實現的。每次都會產生新的數組,和數組中內容的拷貝,因此會耗費性能,因此在多增刪的操做的狀況可優先考慮 LinkList 而不是 ArrayList。
  3. ArrayList 的 toArray 方法重載方法的使用。
  4. 容許存放(不止一個) null 元素,
  5. 容許存放重複數據,存儲順序按照元素的添加順序
  6. ArrayList 並非一個線程安全的集合。若是集合的增刪操做須要保證線程的安全性,能夠考慮使用 CopyOnWriteArrayList 或者使collections.synchronizedList(List l)函數返回一個線程安全的ArrayList類.
  7. 不正確訪問集合元素的時候 ConcurrentModificationExceptionjava.lang.IndexOutOfBoundsException 異常產生的時機和原理。

本文又長篇大論的分析了一波 ArrayList 的源碼,對我我的而言這頗有意義,在查看源碼的過程當中,注意到了平時不多有機會接觸的知識點。固然這只是集合源碼分析的開端,之後還會更細,其餘經常使用集合源碼的分析。若是你們感受我寫的還能夠, 請留言 + 點贊 + 關注。

相關文章
相關標籤/搜索