Java集合框架分析(二)ArrayList分析

相關文章
Java集合框架分析(一)綜合概述

本篇文章主要分析一下 Java 集合框架中的 List 部分,ArrayList,該源碼分析基於JDK1.8,分析工具,AndroidStudio,文章分析不足之處,還請指正!

ArrayList簡介

ArrayList 底層維護的是一個動態數組,每個 ArrayList 實例都有一個容量。該容量是指用來存儲列表元素的數組的大小。它總是至少等於列表的大小。隨着向 ArrayList 中不斷添加元素,其容量也自動增長。ArrayList 不是同步的(也就是說不是線程安全的,同 HashMap、LinkedHashMap 一樣),如果多個線程同時訪問一個 ArrayList 實例,而其中至少一個線程從結構上修改了列表,那麼它必須保持外部同步,在多線程環境下,可以使用 Collections.synchronizedList 方法聲明一個線程安全的 ArrayList,例如:

List arraylist = Collections.synchronizedList(new ArrayList());

源碼分析

類結構定義

首先我們來看一下關於ArrayList的類結構,

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

上面是是 ArrayList 類的定義,它繼承了抽象類 AbstractList,但是真正繼承的方法只有 equals 和 hashCode,別的方法在 ArrayList 中都有自己的重新實現;List 接口在 AbstractList 中已經實現,這裏是爲了表明一下,沒有太大含義;RandomAccess 沒有方法,表明 ArrayList 支持快速隨機訪問;實 現了 Cloneable 接口,以指示 Object.clone() 方法可以合法地對該類實例進行按字段複製,如果在沒有實現 Cloneable 接口的實例上調用 Object 的 clone 方法,則會導致拋出 CloneNotSupportedException 異常;類通過實現 java.io.Serializable 接口以啓用其序列化功能,未實現此接口的類將無法使其任何狀態序列化或反序列化

變量定義

接着我們分析一下 ArrayList 定義的變量。

//序列化ID
private static final long serialVersionUID = 8683452581122892189L;
//數組初始容量大小
private static final int DEFAULT_CAPACITY = 10;
//
private static final Object[] EMPTY_ELEMENTDATA = {};
//elementData存儲ArrayList內的元素
transient Object[] elementData;
//存儲在ArrayList內的元素的數量
private int size;

構造函數

//設定初始容量大小的構造函數
public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    //數組初始化
    this.elementData = new Object[initialCapacity];
}
//無參數構造函數
public ArrayList() {
    super();
    this.elementData = EMPTY_ELEMENTDATA;
}
//將提供的集合轉成數組返回給elementData(返回若不是Object[]將調用Arrays.copyOf方法將其轉爲Object[])
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}

構造函數還是比較簡單的,我們來看看 ArrayList 的最常用的方法 add,

Add(E e)添加數據

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

發現 ArrayList 的 add 方法,只有簡單的兩行代碼,粗略的看一下是在數組 elementData 的尾部添加一個元素 E,那麼到底是怎麼樣操作的呢?我們詳細查看一下源碼,首先進入 ensureCapacityInternal 方法中一探究竟。看這個方法的名字就大概知道這是確保數組容量的,怎麼個確保法呢?

private void ensureCapacityInternal(int minCapacity) {
    //當我們調用ArrayList的無參構造函數時調用此代碼
    if (elementData == EMPTY_ELEMENTDATA) {
        //設置數組容量爲10,默認的大小
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //
    ensureExplicitCapacity(minCapacity);
}
//確保容量大小,modCount自增,並判斷數組大小是否足夠,不夠的話將增大數組
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    //如果最小需要空間比elementData的內存空間要大,則需要擴容  
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

到了這裏還是有點模糊到底怎麼數組擴容的?我們接着看 grow 的方法

//擴容並重新copy一份數組
private void grow(int minCapacity) {

        // oldCapacity爲當前數組大小  
        int oldCapacity = elementData.length;
        // newCapacity爲新容量的大小等於舊容量的1.5倍大小
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果擴充後的容量還是比最少需要的容量還要小的話,就設置擴充容量爲最小需要的容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //判斷最新的容量是否已經超出最大數組範圍,溢出判斷
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
            
        // 調用Arrays.copyOf方法將elementData數組指向新的內存空間時newCapacity的連續空間  
    // 並將elementData的數據複製到新的內存空間  
        elementData = Arrays.copyOf(elementData, newCapacity);
}

我們總結一下 add 操作的過程,首先我們先要判斷是否需要擴容,在擴容判斷裏面,我們主要進行一些判斷,如果當前所需要的容量和當前數組的容量進行比較,如果不夠的話則進行擴容,在擴容的同時則需要判斷是否溢出了,然後將舊的數據數組進行 copy 到新的容量大小的數組中,最後再將需要添加的數據添加到數組的最後一位。

我們接着分析 add 的另一個方法,重載 add(int index, E element) 方法,

//在指定的位置上面插入一個數據
 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++;
    }

中心思想就是,首先判斷指定位置 index 是否超出 elementData 的界限,之後調用 ensureCapacity 調整容量(若容量足夠則不會拓展),調用 System.arraycopy 將 elementData 從 index 開始的 size-index 個元素複製到 index+1 至 size+1 的位置(即 index 開始的元素都向後移動一個位置),然後將 index 位置的值指向 element,同時改變數組的大小。

addAll 方法

接着我們分析一下 addAll 方法。

public boolean addAll(Collection<? extends E> c) {
       Object[] a = c.toArray();
       int numNew = a.length;
       ensureCapacityInternal(size + numNew);  // Increments modCount
       //將a的第0位開始拷貝至elementData的size位開始,拷貝長度爲numNew 
       System.arraycopy(a, 0, elementData, size, numNew);
       size += numNew;
       return numNew != 0;
   }

其實這個方法很好理解,首先將批量添加的數據轉爲數組,然後獲取它的容量大小,判斷一下,如果我們要在數組中插入這一批數據的話,是否需要擴容,經過這個擴容判斷操作,我們的數組容量是足夠了,然後我們開始批量導入數據,其中 System.arraycopy(a, 0, elementData, size, numNew) ;意思就是,將需要導入的批數據從 0 開始,導入到數組從 size 位置開始,導入的大小數量就是需要導入的數量。這個時候,總的數組容量就變成了原先的容量加上導入的容量了。

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 的方法基礎上面修改而來,複製數組的時候主要進行兩步複製,第一步,現將原數組在指定的位置上面向後移動需要的位數,然後第二步再將需要導進去的數據從 index 位置複製進去。

get方法

分析完了 add 方法,我們來分析分析 get 方法內容,get 方法也是比較簡單的

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

首先判斷需要返回的位置是否越界了,其次直接返回數組對應索引裏面的值。非常簡單,我們再看看其他方法。

其餘方法

remove(int index)

我們來看看 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;
  }

同理,因爲涉及到數組的問題,所以首先需要判斷一下是否出現越界的問題,然後就開始將指定刪除位置後面的數據都向前移動一位,然後將最後的一位設置爲 null,最後返回刪除的數據。

刪除一個數據,remove 也有重載方法

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

刪除一個數據,主要是遍歷數組,看看數組中是否存在這個數據,如果存在的話則進行fastRemove,我們進入 fastRemove 中看看

fastRemove(int index)

//刪除指定位置上面的數據
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
}

將指定位置 index 後面的數據全部向前移動一位,最後將最後一位回收掉。

另外我們看下全部刪除數組中的數據方法 clear

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,等帶回收。

removeRange(int fromIndex, int toIndex)

protected void removeRange(int fromIndex, int toIndex) {

    if (toIndex < fromIndex) {
        throw new IndexOutOfBoundsException("toIndex < fromIndex");
    }
    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;
}

執行過程是將 elementData 從 toIndex 位置開始的元素向前移動到 fromIndex,然後將 toIndex 位置之後的元素全部置空順便修改 size。

set(int index, E element)

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

這個方法很簡單,就是修改數組 index 裏面的數據,並返回舊的數據。

indexOf(Object o)

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

返回在數組中第一次出現的值。如果找到這個值的話就返回 index,找不到就返回-1。

trimToSize()

public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = Arrays.copyOf(elementData, size);
    }
}

由於 elementData 的長度會被拓展,size 標記的是其中包含的元素的個數。所以會出現 size 很小但 elementData.length 很大的情況,將出現空間的浪費。trimToSize 將返回一個新的數組給elementData,元素內容保持不變,length 跟 size 相同,節省空間。

總結

以上便是 ArrayList 的源碼內容,總的來說不是很難,接下來我們來總結一下關於 ArrayList 的方方面面。

ArrayList 底層是基於數組來實現的,可以通過下標準確的找到目標元素,因此查找的效率高;但是添加或刪除元素會涉及到大量元素的位置移動,效率低。

ArrayList 提供了三種不同的構造方法,無參數的構造方法默認在底層生成一個長度爲 10 的 Object 類型的數組,當集合中添加的元素個數大於 10,數組會自動進行擴容,即生成一個新的數組,並將原數組的元素放到新數組中。

ensureCapacity 方法對數組進行擴容,它會生成一個新數組,長度是原數組的 1.5 倍 +1,隨着向 ArrayList 中不斷添加元素,當數組長度無法滿足需要時,重複該過程。

ArrayList 不是同步的(也就是說不是線程安全的,同 HashMap、LinkedHashMap 一樣),如果多個線程同時訪問一個 ArrayList 實例,而其中至少一個線程從結構上修改了列表,那麼它必須保持外部同步,在多線程環境下,可以使用 Collections.synchronizedList 方法聲明一個線程安全的 ArrayList,例如:List arraylist = Collections.synchronizedList(new ArrayList());

關於作者

專注於 Android 開發多年,喜歡寫 blog 記錄總結學習經驗,blog 同步更新於本人的公衆號,歡迎大家關注,一起交流學習~

在這裏插入圖片描述

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