ArrayList 源碼分析

ArrayList 源碼分析

前言

ArrayList 算是咱們開發中最常常用到的一個集合了,使用起來很方便,對於內部元素的隨機訪問很快。今天來分析下ArrayList 的源碼,本次分析基於 Java1.8 。java

ArrayList 簡介

先來看下 ArrayList 的 API 描述:數組

從描述裏面來看,ArrayList 是繼承於 AbstractList 的,而且實現了 Serializable, Cloneable, Iterable, Collection, List, RandomAccess 這些接口。安全

  • 實現了 Serializable 是序列化接口,所以它支持序列化,可以經過序列化傳輸。
  • 實現了 Cloneable 接口,能被克隆。
  • 實現了Iterable 接口,能夠被迭代器遍歷
  • 實現了 Collection ,擁有集合操做的方法
  • 實現了 List 接口,擁有增刪改查等方法
  • 實現了 RandomAccess 隨機訪問接口,支持快速隨機訪問,實際上就是經過下標序號進行快速訪問。

先大致瞭解下ArrayList 的特色,而後再從源碼的角度去分析:dom

  1. ArrayList 底層是一個動態擴容的數組結構,初始容量爲 10,每次容量不夠的時候,擴容須要增長 1.5 倍的容量(大多數狀況下是擴容 1.5 倍的,可是在使用 addAll 的時候,可能有例外。)
  2. ArrayList 容許存放重複數據,存儲順序按照元素的添加順序,也容許多個 Null 存在。
  3. 底層使用 Arrays.copyOf 函數進行擴容,每次擴容都會產生新的數組,和數組中內容的拷貝,因此會耗費性能,因此在多增刪的操做的狀況可優先考慮 LinkedList。
  4. ArrayList 並非一個線程安全的集合。若是集合的增刪操做須要保證線程的安全性,能夠考慮使用 CopyOnWriteArrayList 或者使Collections.synchronizedList(List l) 函數返回一個線程安全的 ArrayList 類.

ArrayList 源碼分析

一些屬性

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 序列化 ID
    private static final long serialVersionUID = 8683452581122892189L;

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

    // 一個默認的空數組
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 在調用無參構造方法的時候使用該數組
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

  
    // 存儲 ArrayList 元素的數組
    // transient 關鍵字這裏簡單說一句,被它修飾的成員變量沒法被 Serializable 序列化 
    transient Object[] elementData; // non-private to simplify nested class access

    // ArrayList 的大小,也就是 elementData 包含的元素個數
    private int size;
}
複製代碼

構造方法

內部幾個主要的屬性就這些。再來看下構造方法:函數

// 指定大小的構造方法,若是傳入的是 0 ,直接使用 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);
    }
}
// 調用該構造方法構造一個默認大小爲 10 的數組,可是此時大小未指定,
// 仍是空的,在第一次 add 的時候指定
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 傳入一個集合類
// 首先直接利用Collection.toArray()方法獲得一個對象數組,並賦值給elementData 
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray 出錯的時候,使用Arrays.copyOf 生成一個新數組賦值給 elementData
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        //若是集合c元素數量爲0,則將空數組EMPTY_ELEMENTDATA賦值給elementData 
        this.elementData = EMPTY_ELEMENTDATA;
    }
}
複製代碼

能夠看到,不論是調用哪一個構造方法,都會初始化內部 elementData 。源碼分析

add 方法

接下來從最經常使用的 add 方法看起:性能

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
複製代碼

執行 ensureCapacityInternal(size + 1) 確認內部容量this

private void ensureCapacityInternal(int minCapacity) {
    // 若是建立 ArrayList 時候,使用的無參的構造方法,那麼就取默認容量 10 和最小須要的容量(當前 size + 1 )中大的一個肯定須要的容量。
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
複製代碼

其實這裏的 size 的默認值是 0 ,因此在使用默認構造方法建立 ArrayList 之後第一次執行 ensureCapacityInternal 的時候,要擴容的容量就是 DEFAULT_CAPACITY = 10;spa

private void ensureExplicitCapacity(int minCapacity) {
    // 修改 +1 
    modCount++;
    // 若是 minCapacity 比當前容量大, 就執行grow 擴容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // 拿到當前的容量
    int oldCapacity = elementData.length;
    // oldCapacity >> 1 意思就是 oldCapacity/2,因此新容量就是增長 1/2.
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 若是新容量小於,須要最小擴容的容量,以須要最小容量爲準擴容
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 若是新容量大於容許的最大容量,則以 Inerger 的最大值進行擴容
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 使用 Arrays.copyOf 函數進行擴容。
    elementData = Arrays.copyOf(elementData, newCapacity);
}

// 容許的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

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.5 倍的容量比最小須要的容量(minCapacity)還小,那麼就使用 minCapacity 進行擴容。因此並非每次都是以 1.5 倍進行擴容的。線程

上面講了擴容,擴容好了之後,就執行

elementData[size++] = e;
return true;
複製代碼

進行賦值操做,就完成了一次數據的添加。

再來看下在指定位置添加一個元素:

public void add(int index, E element) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}
複製代碼

先判斷傳入的位置是夠越界。越界就拋出異常

而後確認需不須要擴容,而後再經過 System.arraycopy 方法進行拷貝。

須要注意的是 size - index 表示的是須要移動的元素的數量。也就是 index 後面的元素都要進行移動,這也就是插入效率低的一個緣由,在指定位置插入數據,那麼這個位置後面的數據都要移動,若是是在第 0 個位置插入,意味着全部的元素都要移動。

上面的 add 方法分析完了,而後再來看下另外一個常見的 addAll 方法:

addAll 方法

先看第一個 addAll

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;
}
複製代碼

這裏也很簡單,先轉成數組,拿到長度進行擴容。而後利用 System.arraycopy 函數把傳進來的數組拷貝到現有數組裏面。

再來看第二個 addAll 方法:

這個是在指定位置添加一個集合。

public boolean addAll(int index, Collection<? extends E> c) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(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;
}
複製代碼

這裏也很簡單,基本和使用 add 方法在指定位置添加一個元素差很少。就不在分析了。接下來看看刪除相關的。

remove 方法

看下源碼:

刪除一個指定位置的元素:

public E remove(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    modCount++;
    E oldValue = (E) 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;
}
複製代碼

很簡單,先判斷是夠越界,越界拋出異常。

而後先把要刪除的元素拿出來,存儲在 oldValue ,這裏看到了一個 numMoved ,也就是刪除一個元素須要移動的元素的數量。而後執行 System.arraycopy 進行數組的移動,這裏只移動刪除的 index 後面的元素,通通向前進一位。而後把數組中最後一個元素置爲 null,返回刪除的元素。

刪除一個指定的元素:

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 ,根據循環查找到第一個爲 null 的元素,而後執行 fastRemove(index) 刪除以後,返回 true 刪除成功,能夠看到這裏的 fastRemove 方法和 remove(int index) 是比較相似的,就不講了。
  2. 刪除的元素不爲 null ,和爲 null 邏輯差很少,就是對元素的判斷不一樣,這裏使用的 o.equals(elementData[index]),而爲 null 的時候,使用 elementData[index] == null

set 方法

set 方法就是在指定位置改變一個元素的值

public E set(int index, E element) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    E oldValue = (E) elementData[index];
    elementData[index] = element;
    return oldValue;
}
複製代碼

一樣,先判斷是否越界,越界拋出異常,沒越界直接修改值,把舊值返回。

get 方法

取某個位置的元素:

public E get(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    return (E) elementData[index];
}
複製代碼

一樣,先判斷是否越界,越界拋出異常,沒越界屬於數組的操做,直接返回指定位置的值。

clear 方法

清除數組中的全部元素:

public void clear() {
    modCount++;
    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;
    size = 0;
}
複製代碼

能夠看到是循環把數組中的每一個元素置爲 null,可讓 gc 回收,而後再把數組的長度置爲 0 。下次 add 的時候,仍是直接擴容到長度爲 10.

indexOf 方法

返回元素在集合中的位置

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;
}
複製代碼

和 remove 的時候相似,分爲兩種狀況處理。飯後返回元素在數組中的位置。

最後元素最後出現的位置

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;
}
複製代碼

和 indexOf 操做同樣,只不過是倒序查找第一個元素出現的位置

isEmpty 方法

是否爲空

public boolean isEmpty() {
    return size == 0;
}
複製代碼

能夠看到是根據 size 來判斷的,即便你把 ArrayList 中的每一個元素置爲 null,可是 size 不爲 0 的話,isEmpty 依舊返回 false。

總結

經過上面的分析能夠再次總結下結論:

  1. ArrayList 底層是一個動態擴容的數組結構,初始容量爲 10,每次容量不夠的時候,擴容須要增長 1.5 倍的容量
  2. 增長(add)和刪除(remove)操做會改變 modCount,可是查找(get)和修改(set)不會修改
  3. 從上面能夠看出,增長和刪除均可能涉及到擴容操做,擴容和刪除會移動已有元素的位置,比較低效,可是查找和修改時很高效的。
  4. 從上面看出,ArrayList 對 null 元素是支持的,而且不會限制數量,也不會限制重複元素的增長
  5. 全文沒見 Synchronized 關鍵字,也沒有其它保證線程安全的操做,因此是線程不安全的,可使用CopyOnWriteArrayList 或者使Collections.synchronizedList(List l) 函數返回一個線程安全的 ArrayList 類來保證線程安全。

使用建議:

  • 若是是修改和獲取操做比較多,建議使用 ArrayList ,效率高。
  • 若是增長和刪除操做較多,建議使用 LinkedList(下篇分析),可是若是增長和刪除的操做都在隊尾,不涉及到元素的移動,仍是建議使用 ArrayList ,畢竟 ArrayList 的查找和修改的效率仍是蠻高的。
  • 使用的時候,若是肯定元素的大小,最好能設置下 ArrayList 的容量,避免擴容浪費空間

這篇就講到這裏,下篇來看下 LinkedList。

相關文章
相關標籤/搜索