1. 程式人生 > 實用技巧 >JDK1.8-java.util.ArrayList類原始碼閱讀

JDK1.8-java.util.ArrayList類原始碼閱讀

java.util包下的內容是用得非常多的,而且也是面試必問的,我們先從用得非常多的ArrayList說起。

1、定義

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


ArrayList是一個用陣列實現的集合,元素有序且可重複。實現了RandomAccess介面,支援隨機訪問,實現了Cloneable,Serializable介面,表明可以拷貝和序列化。實現了List介面,表明支援List裡定義的方法

至於RandomAccess介面很多同學可能不太瞭解,下面引用JDK API文件的描述:

Marker interface used by List implementations to indicate that they support fast (generally constant time) random access. The primary purpose of this interface is to allow generic algorithms to alter their behavior to provide good performance when applied to either random or sequential access lists.

大意就是這是一個標記介面,實現了RandomAccess介面就表明支援隨機訪問。此介面的主要目的是允許通用演算法更改其行為,以便在應用於隨機訪問列表或順序訪問列表時提供良好的效能。
後一句話意思是我們通過判斷是否實現了該介面來選擇不同的遍歷方式,因為實現了RandomAccess介面的集合,普通for迴圈比迭代器要快,這點官方文件也有說明:

for (int i=0, n=list.size(); i < n; i++)
list.get(i);
runs faster than this loop:
for (Iterator i=list.iterator(); i.hasNext(); )
i.next();

比如在Collections類裡copy方法就實際運用了:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
             // 這裡判斷了是否實現了RandomAccess,從而選擇了不同的遍歷方式
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

2、欄位

// 預設的初始容量
private static final int DEFAULT_CAPACITY = 10;

// 空的陣列
private static final Object[] EMPTY_ELEMENTDATA = {};

// 這也是一個空的陣列,和EMPTY_ELEMENTDATA的區別下面會講到
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 這是ArrayList裡真正存元素的陣列,它的長度就是ArrayList的容量。為什麼用transient修飾?下面會講
transient Object[] elementData;

// ArrayList裡元素的數量
private int size;

// 要分配的最大陣列大小,一些虛擬機器會在陣列中保留一些header words。嘗試分配更大的陣列可能會導致OutOfMemoryError,所以這裡-8
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

// 序列化的UID
private static final long serialVersionUID = 8683452581122892189L;

2.1、EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的區別

當使用有參建構函式並且初始容量為0時,將EMPTY_ELEMENTDATA賦值給elementData。當使用無參建構函式時,將DEFAULTCAPACITY_EMPTY_ELEMENTDATA賦值給elementData。
他們在第一次新增元素時擴容邏輯不同,EMPTY_ELEMENTDATA新增第一個元素後容量為1。DEFAULTCAPACITY_EMPTY_ELEMENTDATA新增第一個元素後容量為DEFAULT_CAPACITY = 10。

驗證一下:

// 由於是無參建構函式,所以使用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,此時容量為0
ArrayList<String> arrayList1 = new ArrayList<>();
// 新增第一個元素時擴容為10,此時容量為10。注意此時size=1,不要將size和容量混淆。容量就好比一個水桶能裝多少水,size就好比實際上裝了多少水
arrayList1.add("hello");
// 此時使用的是EMPTY_ELEMENTDATA,容量為0
ArrayList<String> arrayList2 = new ArrayList<>(0);
// 新增第一個元素時擴容為1,此時容量為1
arrayList2.add("world");

debug一下:
我們看到arrayList1的容量為10

arrayList2的容量為1

使用EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA還有一個作用就是避免建立多餘的空陣列

2.2、elementData為什麼用transient修飾?

我們知道elementData是真正儲存元素的地方,用transient修飾就代表不能被序列化,那ArrayList序列化還有何意義?
支援序列化需要實現writeObject和readObject方法,真理就在這兩個方法裡:

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

我們看到在序列化時將size和elementData裡實際存在的元素寫出,反序列化也就只需要反序列化實際存在的元素,提高了效能。為什麼不直接將elementData序列化呢?
因為elementData的容量一般情況下大於裡面實際儲存的元素量,直接序列化浪費時間和空間。

3、建構函式

/**
 * 無參建構函式
 */
public ArrayList() {
        // 將DEFAULTCAPACITY_EMPTY_ELEMENTDATA賦值給elementData,當第一次新增元素時,擴容為10
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

/**
 * 指定初始容量的建構函式
 */
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            // 將EMPTY_ELEMENTDATA賦值給elementData,當第一次新增元素時,擴容為1
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

/**
 * 通過已有的集合建立ArrayList
 */
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;
        }
    }

4、新增元素的方法

四個新增元素的方法,分兩類,一類是直接加,一類是帶下標的加,主要邏輯差不多。我們分析下add(E e)方法,理解ArrayList新增元素的邏輯以及擴容的邏輯

/**
 * add方法
 */
public boolean add(E e) {
        // 確定下集合的大小(在往水桶裡裝水前得確認容量是否足夠吧,不夠的話就擴容嘍)
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

/**
 * 確定集合的大小
 */
private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 這裡可以證明new ArrayList()出來的集合,第一次新增元素時擴容為10(面試考點哦)
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

private void ensureExplicitCapacity(int minCapacity) {
        // modCount用於快速失敗(fail-fast)機制
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            // 如果容量不夠就擴容(這裡沒有hashMap裡裝載因子的概念哦,又是一個面試考點,哈哈)
            grow(minCapacity);
    }

/**
 * 擴容方法(高頻考點)
 */
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 容量增加至原來的1.5倍(移位運算比乘除快,大家寫程式碼可以借鑑)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            // 進入到這裡,則代表計算newCapacity時發生了溢位,直接擴容為所需要的容量
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            // 當計算的新容量比MAX_ARRAY_SIZE還大時,則呼叫hugeCapacity處理
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        // 將原陣列拷貝到長度為newCapacity的新數組裡
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        // 當所需要的容量大於MAX_ARRAY_SIZE時,擴容為Integer.MAX_VALUE
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

是不是有很多同學直接跳過了上面的程式碼?哈哈,這裡總結下ArrayList的擴容機制:

5、刪除元素的方法

/**
 * 刪除指定下標的元素
 */
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);
        // 將最後一個元素設為null,方便垃圾回收
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

/**
 * 刪除指定的元素(注意這裡只會刪除第一次匹配到的該元素)
 */
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;
    }

/**
 * 刪除與傳入的集合裡相同的元素
 */
public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }

/**
 * 刪除符合條件的元素
 */
public boolean removeIf(Predicate<? super E> filter) {
        // 太多我就不貼了
    }


/**
 * 刪除指定下標之間的元素(protected修飾,我們一般用不到)
 */
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;
    }

6、修改元素的方法

/**
 * 修改指定下標的元素
 */
public E set(int index, E element) {
        // 檢查下標是否越界
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

7、查詢元素的方法

/**
 * 根據下標查詢元素
 */
public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }