源碼分析 (一) ArrayList JDK 1.8 源碼分析

簡介

本篇分析 ArrayList 的源碼,在分析以前先跟你們談一談數組。數組多是咱們最先接觸到的數據結構之一,它是在內存中劃分出一塊連續的地址空間用來進行元素的存儲,因爲它直接操做內存,因此數組的性能要比集合類更好一些,這是使用數組的一大優點。可是咱們知道數組存在致命的缺陷,就是在初始化時必須指定數組大小,而且在後續操做中不能再更改數組的大小。在實際狀況中咱們遇到更多的是一開始並不知道要存放多少元素,而是但願容器可以自動的擴展它自身的容量以便可以存放更多的元素。ArrayList 就可以很好的知足這樣的需求,它可以自動擴展大小以適應存儲元素的不斷增長。它的底層是基於數組實現的,所以它具備數組的一些特色,例如查找修改快而插入刪除慢。本篇咱們將深刻源碼看看它是怎樣對數組進行封裝的。首先看看它的成員變量和三個主要的構造器。java

初始化及構造函數

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

    /** * 默認初始化的容量 */
    private static final int DEFAULT_CAPACITY = 10;

    /** * 初始化一個空實例的共享空數組實例。 */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /** *用於默認大小的空實例的共享空數組實例。咱們將此與 empty _ elemtdata 區分開來, 以瞭解 *添加分離第一元素時充氣的程度 */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /** * 元素對象數據 */
    transient Object[] elementData; // non-private to simplify nested class access

    /** * 當前 集合 的大小 * * @serial */
    private int size;

    /** * 建立 ArrayList 的時候直接給元素對象開闢一個指定的容量大小 * 不然 直接初始化一個空的元素對象 * * @param * @throws * */
    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);
        }
    }

    /** * 若是直接 new 一個空的構造函數的 ArrayList 內部直接建立一個默認空的對象數組 */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /** * * @param c 外部直接傳入一個集合 * @throws NullPointerException if the specified collection is null */
    public ArrayList(Collection<? extends E> c) {
    	//將外部傳入進來的集合數據轉化爲數組賦值給 元素對象數組
        elementData = c.toArray();
        //更新集合大小
        if ((size = elementData.length) != 0) {
            // 判斷引用的數據類型,並將引用轉換成 Object 數組引用
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 若是傳入進來的是一個空的集合,那麼直接建立一個空數組
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
複製代碼

能夠看到 ArrayList 的內部存儲結構就是一個 Object 類型的數組,所以它能夠存聽任意類型的元素。在構造ArrayList 的時候,若是傳入初始大小那麼它將新建一個指定容量的 Object 數組,若是不設置初始大小那麼它將不會分配內存空間而是使用空的對象數組,在實際要放入元素時再進行內存分配。下面再看看它的增刪改查方法。數組

添加

/** * @param 將 e 元素添加到當前 elementData 中 * @return 返回是否添加成功 */
public boolean add(E e) {
    // 每次添加數據的時候都要檢查當前數組容量是否須要擴容
    ensureCapacityInternal(size + 1);  
    // 直接賦值
    elementData[size++] = e;
    return true;
}

/** * * * @param index 根據外部傳入的 index 來進行存儲數據 * @param element element to be inserted * @throws IndexOutOfBoundsException {@inheritDoc} */
public void add(int index, E element) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
	//檢查是否須要擴容
    ensureCapacityInternal(size + 1);  
    //將須要插入的 index 位置日後移動一位
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    //在 數組的 index 位置直接存入 元素
    elementData[index] = element;
    size++;
}
複製代碼

刪除

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)
        //將index後面的元素向前挪動一位
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 置空引用
    elementData[--size] = null; 
	//返回被刪除的元素
    return oldValue;
}

/** * * @param 刪除外部傳入的對象 * @return 是否刪除成功 */
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;
}

   /** * 刪除所有 * be empty after this call returns. */
    public void clear() {
        modCount++;

        // 直接把容量空間所有置爲 NULL 交於 GC
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }
複製代碼

改變元素

/**
 * @return 返回舊元素
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
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;
}
複製代碼

查詢數據

/**
 * @param  根據索引直接從數據元素中獲取
 * @return 返回取出的數據
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E get(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    return (E) elementData[index];
}
複製代碼

每次添加一個元素到集合中都會先檢查容量是否足夠,不然就進行擴容,擴容的細節下面會講到。咱們先看具體增刪改查要注意的地方。安全

增(添加):僅是將這個元素添加到末尾。操做快速。bash

增(插入):因爲須要移動插入位置後面的元素,而且涉及數組的複製,因此操做較慢。數據結構

刪:因爲須要將刪除位置後面的元素向前挪動,也會設計數組複製,因此操做較慢。dom

改:直接對指定位置元素進行修改,不涉及元素挪動和數組複製,操做快速。函數

查:直接返回指定下標的數組元素,操做快速。性能

經過源碼看到,因爲查找和修改直接定位到數組下標,不涉及元素挪動和數組複製因此較快,而插入刪除因爲要挪動元素,涉及到數組複製,操做較慢。而且每次添加操做還可能進行數組擴容,也會影響到性能。下面咱們看看ArrayList是怎樣動態擴容的。ui

集合底層數組擴容具體操做

private void ensureCapacityInternal(int minCapacity) {
	//若是當前元素數據爲空的時候
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    	// 和默認的擴容容量比較,取出最大值。
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
	// 數組初始化完畢進行下一步
    ensureExplicitCapacity(minCapacity);
}

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

    // 若是最小的擴容容量 比 當前的 元素數據還要大的時候進行擴容,下面是擴容的核心代碼
    if (minCapacity - elementData.length > 0)
        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 = minCapacity;
            //主要檢查新的容量是否已經超過最大容量了
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //返回新擴容的元素數據
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

複製代碼

每次添加元素前會調用 ensureCapacityInternal 這個方法進行集合容量檢查。在這個方法內部會檢查當前集合的內部數組是否仍是個空數組,若是是就新建默認大小爲 10 的 Object 數組。若是不是則證實當前集合已經被初始化過,那麼就調用 ensureExplicitCapacity 方法檢查當前數組的容量是否知足這個最小所需容量,不知足的話就調用 grow 方法進行擴容。在 grow 方法內部能夠看到,每次擴容都是增長原來數組長度的一半,擴容其實是新建一個容量更大的數組,將原先數組的元素所有複製到新的數組上,而後再拋棄原先的數組轉而使用新的數組。至此,咱們對 ArrayList 中比較經常使用的方法作了分析,其中有些值得注意的要點:this

  1. ArrayList 底層實現是基於數組的,所以對指定下標的查找和修改比較快,可是刪除和插入操做比較慢。
  2. 構造 ArrayList 時儘可能指定容量,減小擴容時帶來的數組複製操做,若是不知道大小能夠賦值爲默認容量 10。
  3. 每次添加元素以前會檢查是否須要擴容,每次擴容都是增長原有容量的一半。
  4. 每次對下標的操做都會進行安全性檢查,若是出現數組越界就當即拋出異常。
  5. ArrayList 的全部方法都沒有進行同步,所以它不是線程安全的。
  6. 以上分析基於 JDK1.8,其餘版本會有些出入,所以不能一律而論。
相關文章
相關標籤/搜索