ArrayList 源碼分析(基於 JDK 1.8)

ArrayList 源碼分析

原文地址:GitHub
ArrayList 是 Java 中比較常用的集合之一。它實現了 List, RandomAccess, Cloneable, java.io.Serializable 這四個接口,List 接口是爲了讓 ArrayList 去實現 List 的各種方法,實現 RandomAccess 是爲了讓 ArrayList 支持快速快速隨機訪問,實現 Cloneable 是爲了讓 ArrayList 支持 clone() 方法,實現 Serializable 是爲了讓 ArrayList 支持序列化。

上面實現的 List 和 Serializable 接口可能大家都沒什麼疑惑,但是 RandomAccess 和 Cloneable 呢?我們看 RandomAccess 和 Cloneable 的源碼,發現這兩個接口都是空的。

在這裏插入圖片描述
(../image/arraylist_1_2.png)]
其實這兩個空的接口都是標記型接口,什麼意思呢?就是來標記一個類是否屬於使用快速隨機訪問個 clone() 方法的標記。

比如標記型接口是怎麼標記是否可以使用快速隨機訪問的呢?在集合類 Collections 中我們可以看到:

在這裏插入圖片描述

是否實現了 RandomAccess 接口使用的搜索方法是不一樣的。那麼 Cloneable 的標記呢?我們知道 clone() 方法是 Object 類的,所以直接來看 Object 的源碼:
在這裏插入圖片描述

我們看到了,如果沒有實現 Cloneable 接口而使用 clone() 方法的話是會拋出 CloneNotSupportedException。所以類似於 RandomAccess 和 Cloneable 這樣沒有實體的接口在 Java 中被稱爲標記型接口,用來標記一個類或者接口。

主要參數

接下來我們看一下 ArrayList 的主要參數:

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

// 這是一個空數組,用來初始化真正保存數據的數組
private static final Object[] EMPTY_ELEMENTDATA = {};

// 這個也是空數組,用來初始化的,它與上面的空數組的不同在源碼中解釋如下
// We distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when first 
// element is added.
// 我們將此與EMPTY_ELEMENTDATA區別開來,以瞭解添加第一個元素時要充氣多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// ArrayList 中用來真正保存數組的 Object 數組
transient Object[] elementData;

// ArrayList 中保存的元素數量
private int size;

構造方法

接下來我們看一下 ArrayList 的構造方法,ArrayList 有三個構造方法:

// 傳入一個容量
public ArrayList(int initialCapacity) {
        // 容量大於 0,就新建一個大小爲容量的 Object 數組
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            // 等於 0 就初始化爲參數空數組
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            // 要不就拋出一個異常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

// 不傳入容量時,則初始化爲第二個空數組
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

// 傳入一個集合
public ArrayList(Collection<? extends E> c) {
        // 數組就爲集合轉數組
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // 元素數量不爲 0
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                // 如果類型不爲 Object 數組,則使用 copyof
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            // 元素數量爲 0,則替換爲空數組 
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

我們來看一下 Arrays 中的 copyof 幹了啥?

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        // 如果原來的數組是 Object 數組就返回 Object 數組,如果不爲 Object 數組,則返回一個傳入的新         // 類型的數組 
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

我們看到在 ArrayList 的第三個構造方法中,傳入的新類型也是 Object,所以它這裏調用 Arrays.copyof 是爲了將 elementData 轉爲 Object 數組。

add()、set()、get()

在集合類中,get()、和 add() / set() / put() 無疑是最重要的幾個方法,我們先來看看 ArrayList 中的 add() 方法,ArrayList 中有兩個 add() 方法:

// 將元素添加到數組末尾
public boolean add(E e) {
        // 該方法我們通過方法名就可以看出是爲了確保內部容量足夠,下面會講到
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 然後將元素 e 存入 size++ 位置
        elementData[size++] = e;
        return true;
    }

在看第二個 add() 方法之前,我們需要先看一下 System.arraycopy() 這個方法:

@FastNative
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

這個方法在源碼中是一個 native 方法,是一個快速拷貝數組的方法,這個方法是指我們將數組 src 從位置 srcPos 開始,拷貝到從 destPos 位置開始的數組 dest 中,一共拷貝 length 個元素:

在這裏插入圖片描述

現在我們可以來看看第二個 add() 方法了:

// 將元素添加到數組固定位置
public void add(int index, E element) {
        // 插入的 index 是不能比 size 大,比 0 小
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        // 確保內部容量足夠
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 數組 index 位置的元素爲 element
        elementData[index] = element;
        // 數組元素數量 ++
        size++;
    }

那我們 ArrayList 中調用的 System.arraycopy(elementData, index, elementData, index + 1,size - index) 其實就是把數組 elementData 中的數組從 index 位往後移一位。

ArrayList 還提供了 set() 方法:

public E set(int index, E element) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        E oldValue = (E) elementData[index];
        // 將 index 的元素換成新元素
        elementData[index] = element;// 返回舊元素
        return oldValue;
    }

ArrayList 的 get() 比較簡單,就是返回了數組中指定 index 的元素:

public E get(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        return (E) elementData[index];
    }

ArrayList 中的擴容

在上面的 add() 方法中,我們看到了有一個保證內部容量的方法 ensureCapacityInternal(),那這個方法具體是怎麼樣的呢?

// minCapacity 爲要保證內部容量的最低值,add() 方法中爲 size+1
private void ensureCapacityInternal(int minCapacity) {
        // 判斷保存元素的數組是不是爲空數組
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 是空數組就取傳入值與默認容量的最大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 那這個方法又是幹什麼呢?
        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;
        // 新容量等於 舊容量 + 舊容量 * 2
        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);
    }

而 hugeCapacity() 這個方法是給 ArrayList 取極限值容量的:

private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        // 需要的值大於 MAX_ARRAY_SIZE 就取 Integer.MAX_VALUE,否則取 MAX_ARRAY_SIZE
        // 其中 MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

再看看 remove() 方法

ArrayList 中的 remove() 方法也有兩個,我們先來看看第一個:

// 通過下標移除元素
public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        modCount++;
        // 拿到舊元素
        E oldValue = (E) elementData[index];
        // 移除元素過後數組需要移動距離,size - index - 1,就是 index 後面所有的元素
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 這個方法上面已經說過,在這裏呢,主要是將數組後 numMoved 個元素向前移一位
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 將 index 爲 size -1 的元素也就是最後一個元素置爲空,並且元素個數減一
        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;
    }

那麼快速移除是怎麼具體實現呢?

// 我們可以看到,fastRemove() 方法與第一個 remove() 方法差不多一樣的
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
    }

總結

通過這次的 ArrayList 源碼分析,我們可以看到 ArrayList 的查詢是非常快的,但是增刪都比較慢,add() 如果是直接添加到末尾還好說,如果添加到中間,數組後面的元素是全部需要移動的,而刪除時,不管是哪種方式,都是需要移動數組,就導致增刪比較慢。當然這裏的慢一般是比較少見的,平時我們使用的場景的數組元素的個數一般來說是感受不到這種快慢的,但是呢,我們瞭解了其內部結構過後我們也可以按照場景選擇不同的集合類了。

好了,ArrayList 的源碼就分享到這裏了,喜歡的話請關注我的個人微信公衆號:

在這裏插入圖片描述

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