Java集合源碼學習—ArrayList

又是熱愛學習的一天… 今天準備學習一下 ArrayList 的源碼,研究它都幹了什麼。
在這裏插入圖片描述在這裏插入圖片描述

總結 (先看結論,再尋找如何得出的此結論)

  • ArrayList 是一種以數組實現的 List,與數組相比,它具有動態擴展的能力,因此也可稱之爲 動態數組

  • ArrayList 實現了 List,提供了基礎的添加、刪除、遍歷等操作。

  • ArrayList 實現了 RandomAccess,提供了隨機訪問的能力。

  • ArrayList 實現了 Cloneable,可以被克隆。

  • ArrayList 實現了 Serializable,可以被序列化。

1. ArrayList 的成員變量

先看看 ArrayList 的成員變量,我把它的每一個成員變量都加了註釋用以解釋這個變量有何作用。

 	/**
     * 默認初始容量,也就是說,使用 new ArrayList() 創建的 ArrayList ,它的初始容量爲 DEFAULT_CAPACITY;
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空數組,使用 new ArrayList(0) 創建 ArrayList 時,使用的數組
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 空數組,使用 new ArrayList() 創建 ArrayList 時,使用的數組。
     * 在添加第一個元素的時候,會將這個數組的容量初始化爲 DEFAULT_CAPACITY 大小
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 存放真正的元素數據的數組
     * 在添加第一個元素的時候,會使 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 數組容量初始化爲 DEFAULT_CAPACITY 大小,這和上面那句話對應起來了。
     * 疑問?這裏加上 transient 關鍵字的意思應該是爲了不讓序列化這個數組裏面的內容,也就是我們存進 ArrayList的真實數據,可是經過測驗,卻可以序列化該數組裏面的數據。
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * ArrayList 所包含的實際元素個數,而不是 ArrayList 的長度
     * 它和 elementData.length 的區別是,size是實際元素個數,elementData數組裏,可能有4個實際元素,6個空元素,所以 elementData.length,代表了 elementData 的長度,它裏面包含了空元素。
     */
    private int size;

2. ArrayList 的構造函數

2.1 指定容量構造函數:ArrayList(int initialCapacity)

    /**
     * 傳入初始容量,如果容量大於0,就將 elementData 初始化爲對應大小。如果等於0,那就使用空數組:EMPTY_ELEMENTDATA。如果小於0,就拋異常了。
     * @param initialCapacity 傳入指定的容量
     */
    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);
        }
    }

2.2 默認構造函數:ArrayList()

	/**
     * 如果不傳參數,那就使用空數組:DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
     * 目前 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的容量爲0,要等到添加第一個元素時。它纔會初始化爲 DEFAULT_CAPACITY 的大小。
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

2.3 傳入集合構造函數:ArrayList()

     /**
     * 傳入一個 Collection 集合,並調用 toArray() 方法將它裏面的內容初始化給 elementData。
     * 然後再判斷元素個數是否爲0,如果爲0,就將 elementData 初始化爲 EMPTY_ELEMENTDATA 這個空數組
     * @param c
     */
    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;
        }
    }

這裏請注意!! 該構造方法中寫道:c.toArray might (incorrectly) not return Object[] (see 6260652)
意思是: c.toArray方法返回的可能不是 Object[] 類型,詳情見JDK bug編號 6260652

那麼這裏的 if 判斷是說,如果 elementData 不是 Object[] 的類型,就通過 copyOf 這個方法將不是 Object[] 的類型的 elementData 數組(它有可能是String[],int[]…不管它是什麼,暫時不管),轉換成 Object[] 類型。

這句話有點繞,簡單來說就是:如果 elementData 如果不是 Object[] 的類型,那就通過 copyOf 方法把它轉換成 Object[] 的類型

2.4 疑問:爲什麼 elementData 會可能不是 Object[] 的類型呢?

爲什麼這裏會有這一步操作呢?爲什麼 elementData 會不是 Object[] 的類型呢?下面舉例子解釋:

我們首先來看一下這個構造器裏面的內容,我們聚焦在第二層的這個 if 判斷上,它判斷了 elementData.getClass() 是否等於 Object[].class。那麼,elementData 是怎麼來的呢?構造器第一句就告訴我們了,他是 Collection 類型的參數 c 調用toArray方法初始化來的。 也就是說,這個 if 判斷的是 c.toArray 方法返回的類型是否是 Object[] 類型。

先理解上面這段說明,下面就舉一個栗子來分析說明:

	// 1、首先使用工具類 Arrays 的  asList 方法,將數組轉換成 List。
	List<String> myList = Arrays.asList("123456", "ABCDEF");
	
	// 2、模擬進入構造器,構造器需要 Collection 類型的數據,爲什麼這裏傳 List 也可以呢?
	//因爲 ArrayList 實現與 List 接口,List 接口又繼承於 Collection 類,所以 ArrayList 也算是 Collection 的子類。所以這裏傳 List 也 OK。
	List<String> arrayList = new ArrayList<>(myList);
	
	// 這裏的 myList 就是構造器裏面的 c 變量。 myList 爲實參, c爲形參。
	// 根據上面的理解,如果 c.toArray方法返回的類型,也就是這裏的 myList.toArray 方法返回的類型不爲 Object[] 類型,那就做轉換動作,那麼 myList 的類型到底是什麼呢?
	System.out.println(myList.toArray());

打印結果爲:

	[Ljava.lang.String;@27c170f0

結果說明 c.toArray方法返回的類型 還真有不是 Object[] 類型的,所以構造器裏面存在這個if判斷操作。

這個時候,可能有同學要問了:

  1. 爲什麼 myList.toArray 方法返回的不是 Object[] 類型呢???
  2. 在例子中 myList 已經是一個 List 了,我爲什麼不直接用它呢,爲什麼還要把它當作參數再創建新的ArrayList呢,我直接用
    myList 不就得了嗎?

2.5 解答問題1:爲什麼 myList.toArray 方法返回的不是 Object[] 類型呢???

首先看例子中,我們是使用工具類 Arrays 的 asList 方法,將字符串數組轉換成 List。那麼我們點進 asList 方法看一下源碼:

	@SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new Arrays.ArrayList<>(a);
    }

可以看見它只不過是 new 了一個 Arrays.ArrayList<>(a) 對象而已。
那麼這個 Arrays.ArrayList<> 是個什麼東西呢?它其實就是 Arrays 類裏面的一個內部類,它就在 asList 方法的下面,源碼中他們緊挨着。部分代碼如下:

Arrays.ArrayList<> 的源碼

 private static class ArrayList<E> extends AbstractList<E>
            implements RandomAccess, java.io.Serializable
    {
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }

        @Override
        public int size() {
            return a.length;
        }

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

也就是說 Arrays.asList 方法返回的是 Arrays 的一個內部類,它繼承於 AbstractList,所以它返回的並不是 java.util.ArrayList。
現在知道 Arrays.asList 方法返回的是什麼之後,再來看爲什麼它調用 toArray() 方法之後,返回的不是 Object[] 類型。

現在將目光聚焦在我貼出來的 Arrays.ArrayList 這個內部類的部分源碼,看最後一個方法,這是啥!!!!!其實他重寫了父類的toArray()方法,所以當我們在使用 Arrays.asList 方法創建出來的對象的 toArray 方法時,調用的是他自己重寫的方法!!

現在再看他是如何重寫的,它是 clone 了一份 成員變量 a 的數據,a 是我們創建對象時傳進來的,一直往上追,可以發現 a 裏面就是我們用例中傳的字符串數組,如果我們用例中寫的兩個int類型的數據,那麼此時的 a 就會是這個int數組。所以 toArray 方法其實返回的是一個數組,它並不是一個真正的List。

現在明白爲啥例子中打印出來是字符串數組類型的,而不是 Object[] 類型了吧。

2.6 解答問題2:在例子中 myList 已經是一個 List 了,我爲什麼不直接用它呢,爲什麼還要把它當作參數再創建新的ArrayList呢,我直接用 myList 不就得了嗎?

滿足願望,那就先使用 myList 增加一個元素試試:

	myList.add("HyugaNeji");

結果顯示:

    java.lang.UnsupportedOperationException
	at java.util.AbstractList.add(AbstractList.java:148)
	at java.util.AbstractList.add(AbstractList.java:108)
	at AsListTest.main(AsListTest.java:25)

結果是拋異常了,原因是 myList 的類型是 Arrays.ArrayList 類型的,它自己沒有實現 add 方法,但是它繼承的父類 AbstractList 實現了,所以這裏調用 add ,是調用到了它的父類的 add 方法,而它的父類是實現了 add 方法的,其內容如下:

public boolean add(E e) {
  	 add(size(), e);
	 return true;
}

發現這個方法又調用了 另一個 add 方法,繼續往下看,在 147 行的位置發現了這個方法:

public void add(int index, E element) {
     throw new UnsupportedOperationException();
}

結果已經很明顯了,它的實現方法就是拋異常…

所以,Arrays.asList 創建的所謂的List,是不可以使用 add、set、remove這些方法的,既然這些方法都不能用,我要他有何用,所以纔會使用它作參數再創建新的ArrayList。所以纔有了構造器裏面的那段代碼。

至於,Arrays.ArrayList爲什麼不直接返回一個 java.util.ArrayList 的原因,我現在暫時還沒搞清楚,可能是設計模式層面的東西,因爲它現在這樣實現,不就是適配器模式嗎?

一口氣學習了這麼多,可累壞了。歇會兒
在這裏插入圖片描述

3. 保證數組容量安全的核心輔助方法

先介紹幾個保證數組容量安全的核心輔助方法

3.1 ensureCapacityInternal 方法

private void ensureCapacityInternal(int minCapacity) {
		ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

ensureCapacityInternal 方法的意思是 確保 ArrayList 內部容量的意思。如果容量不夠裝了就進行擴容,確保容量。
它的實現是調用了兩個方法,它通過調用 calculateCapacity 方法,拿到返回值,再傳給 ensureExplicitCapacity 方法。下面我們先看 calculateCapacity 方法做了什麼。

3.2 calculateCapacity 方法

calculateCapacity 方法,看名字就知道它是 計算容量的,它的目的是返回 ArrayList 要存放數據的最小的目標容量。
它通常在 add 數據的時候被使用到,參數 minCapacity 的意思是存放數據需要最小的容量,它是: ArrayList 的實際存放元素個數 + 新增的個數

private static int calculateCapacity(Object[] elementData, int minCapacity) {
   	 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
      	  return Math.max(DEFAULT_CAPACITY, minCapacity);
   	 }
   	 return minCapacity;
}

首先判斷 elementData 的引用和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的引用是否是相同的,也就是判斷是否調用了無參構造器。
因爲如果調用了無參構造器,那麼 elementData 的容量 == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的容量 == DEFAULT_CAPACITY(原因詳見構造器函數)
如果是相同的,那麼 elementData 的初始容量就是 DEFAULT_CAPACITY,所以比較 DEFAULT_CAPACITY 和 minCapacity ,誰大返回誰。

換句話說就是:如果調用了無參構造器,那麼最小容量最小就是10。這個10容量,可能是幾個實際元素 + 幾個空元素;

如果不是調用的無參構造器,那麼就直接返回 minCapacity。也就是說:我新增元素的個數 + 數組中已有的元素個數 = 最小容量minCapacity,那你最少的給我準備 minCapacity 個數的容量,纔夠裝我的數據。

3.3 ensureExplicitCapacity 方法

ensureExplicitCapacity 方法決定了 ArrayList 要不要擴容

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
 }
  1. 首先累加修改次數
  2. 如果我實際要存放數據的個數(minCapacity) 減去 elementData.length 大於0,
    也就是說,我要存放數據的個數大於數組的長度,這個數組裝不下了,就需要調用 grow() 方法 擴容了。

3.4 允許最大數組長度

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

減8的目的是爲了留位置存放數組的長度,因爲數組自己不能計算長度,需要留個位置記錄一下

3.5 擴容的方法 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);
  }

grow 擴容方法,傳入參數(minCapacity) 告訴這個方法,我需要存放的數據的個數,也就是最小的容量,你的容量最小得等於我的個數,不然就不夠裝啊。

  1. 首先,算出數組舊的容量,賦值給 oldCapacity 變量
  2. 然後計算出 舊容量的1.5倍,在賦值給新容量 newCapacity。這裏 oldCapacity >> 1 相當於除以2,在加上原來的就是1.5倍了。JDK1.6的做法是:int newCapacity = (oldCapacity*3)/2+1,這裏位運算的速度是要比整除效率高。我這是JDK1.8。
  3. 接着判斷,如果 newCapacity - minCapacity 如果小於 0 表示新容量比最小我要求容量還要小,也就是擴容後你還不夠裝的話,那就使用我要求的最小容量吧。將最小容量的值賦值給新容量變量。
  4. 再接着判斷,如果 newCapacity - MAX_ARRAY_SIZE 大於 0 表示 新容量比允許最大數組長度都還要大,那咋辦。那就只有請出我的大寶貝了,啊,呸…請出巨大容量函數:hugeCapacity(minCapacity)。並將它的返回值賦值給新容量。
  5. 最後,再將老數組的裏面的數據拷貝到新數組裏面去。

下面看看 巨大容量函數:hugeCapacity(minCapacity) 做了啥。

3.6 hugeCapacity 方法

 private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
 }
  1. 首先,該函數判斷了 minCapacity 是否是小於 0,如果是,那就表示內存溢出啦,直接拋錯誤。因爲int是有範圍的,超出了整個範圍之後,就會變成一個負數。
  2. 然後判斷,我所需要的容量(minCapacity),是否是大於 MAX_ARRAY_SIZE,如果是那就返回 Integer.MAX_VALUE:整型的最大值 2^{31}-1。否則就返回 MAX_ARRAY_SIZE

所以 ArrayList 擴容的核心思想是:擴容原來數組長度的1.5倍,然後再將老數組裏面的數據拷貝到新數組中

4. 平時常用的方法

4.1 添加元素方法:add(E e)

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
}
  1. 首先它調用了 ensureCapacityInternal 方法確保容量,檢查是否需要擴容,關於這個方法的詳細解釋和調用鏈在前面已經學習過了,這裏就不多解釋了。
  2. 然後進行賦值

4.2 添加元素到指定位置方法:add(int index, E element)

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

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
  1. 首先 調用 rangeCheckForAdd 函數,檢查指定位置是否越界,這個函數只有 add 和 addAll 的時候使用。
  2. 確保容量,檢查是否需要擴容
  3. 把指定索引位置後的元素都往後挪一位;
  4. 在指定索引位置放置插入的元素;
  5. 累加實際存放元素個數。

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;
 }
  1. 首先,將參數 c 的數據拷貝到數組 a 中
  2. 算出 a 的長度,也就是這次於要添加數據的個數
  3. 確保容量,檢查是否需要擴容
  4. 把數組a中的元素拷貝到elementData的尾部;
  5. 累加實際存放元素個數。

4.4 獲取指定索引位置的數據:E get(int index)

public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }
        private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

@SuppressWarnings("unchecked")
E elementData(int index) {
	return (E) elementData[index];
}

首先檢查索引是否越界,這裏只檢查是否越上界,如果越上界拋出IndexOutOfBoundsException異常,如果越下界拋出的是 ArrayIndexOutOfBoundsException異常。
然後在返回指定索引位置處的元素;

4.5 移除指定位置的元素:remove(int index)

 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;
 }
  1. 檢查索引是否越界;
  2. 獲取指定索引位置的元素;
  3. 如果刪除的不是最後一位,則其它元素往前移一位;(計算後 numMoved 如果等於0,那麼表示刪除的最後一位)
  4. 將最後一位置爲null,方便GC回收;
  5. 返回刪除的元素。

注意:ArrayList刪除元素的時候並沒有縮小容量。

4.5 刪除指定對象:remove(Object o)

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
}
  1. 如果指定對象爲 null的話,單獨處理,ArrayList是可以存儲null 的。 兩段處理邏輯本質並沒有變化:找到第一個等於指定元素值的元素
  2. 調用 fastRemove方法 快速刪除;

fastRemove(int index)相對於remove(int index)少了檢查索引越界的操作,並且不會返回已刪除的值。

4.6 求兩個集合的交集:retainAll(Collection<?> 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;
        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;
}
  1. 遍歷elementData數組;
  2. 如果元素在c中,則把這個元素添加到elementData數組的w位置並將w位置往後移一位;
  3. 遍歷完之後,w之前的元素都是兩者共有的,w之後(包含)的元素不是兩者共有的;
  4. 將w之後(包含)的元素置爲null,方便GC回收;

4.7 根據指定集合,刪除原數組中與集合數據相同的數據:removeAll(Collection<?> c)

public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
}

與retainAll(Collection<?> c)方法類似,只是這裏保留的是不在c中的元素。

4.8 清楚所有元素:clear()

public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
}
  1. 首先記錄修改次數
  2. 循環 size ,將 elementData 裏面的元素設置爲 null,方便 GC 工作。
  3. 最後將 size 設置爲 0。

5. 解答上面疑問:transient Object[] elementData 爲何 elementData 要設置成 transient

transient 關鍵字的意思是不讓序列化該關鍵字修飾的內容
而 ArrayList 它是實現了 java.io.Serializable 接口,這表示它可以被序列化,但是真正存儲數據的數組卻修飾成了不讓序列化。那麼這麼做有什麼意義呢?

原因是:ArrayList 源碼裏面有 writeObject() 和 readObject() 兩個方法,這兩個方法聲明爲private,在只有再 java.io.ObjectStreamClass#getPrivateMethod() 方法中通過反射獲取到 writeObject() 這個方法

這樣做的目的是:爲了自己控制序列化的方式! 因爲 elementData 是一個緩存數組,它通常會預留一些容量,等容量不足時再擴充容量, 所以ArrayList的設計者將elementData設計爲transient,然後在writeObject方法中手動將其序列化,並且只序列化了實際存儲的那些元素,而不是整個數組,這樣減少了空間佔用。

6. 總結

  • ArrayList內部使用數組存儲元素,當數組長度不夠時進行擴容,每次擴容1.5倍空間,remove 和 clear 不會讓 ArrayList進行縮容
  • ArrayList添加元素到中間比較慢,因爲要挪動元素
  • ArrayList從中間刪除元素也比較慢,因爲要挪動元素
  • ArrayList支持隨機訪問,通過索引訪問元素很快。

在這裏插入圖片描述


技 術 無 他, 唯 有 熟 爾。
知 其 然, 也 知 其 所 以 然。
踏 實 一 些, 不 要 着 急, 你 想 要 的 歲 月 都 會 給 你。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章