【JDK1.8】JDK1.8集合源碼閱讀——ArrayList

1、前言

在前面幾篇,咱們已經學習了常見了Map,下面開始閱讀實現Collection接口的常見的實現類。在有了以前源碼的鋪墊以後,咱們後面的閱讀之路將會變得簡單不少,由於不少Collection的結構與Map的相似,甚至有很多是直接用了Map裏的方法。接下來讓咱們一塊兒來看一下ArrayList的源碼。java


2、ArrayList結構概覽

ArrayList結構

顧名思義,ArrayList的結構實際就是一個Object[]。因此它的特性很明顯,插入一個元素的時候,是耗時是一個常量時間O(1),在插入n個元素的時候,須要的時間就是O(n)。其餘的操做中,運行的時間也是一個線性的增加(與數組中的元素個數有關)。算法


3、ArrayList源碼閱讀

3.1 ArrayList的繼承關係

ArrayListExtends

其中值得一提的是RandomAccess接口,該接口的目的是這麼說的:數組

List 實現所使用的標記接口,用來代表其支持快速(一般是固定時間)隨機訪問。此接口的主要目的是容許通常的算法更改其行爲,從而在將其應用到隨機或連續訪問列表時能提供良好的性能。dom

對於順序訪問的list,好比LinkedList,使用Iterator訪問會比使用for-i來遍歷list更快。這一點其實很好理解,當對於LinkedList使用get(i)的時候,因爲是鏈表結構,因此每次都會從表頭開始向下搜索,耗時確定會多。


對於實現RandomAccess這個接口的類,如ArrayList,咱們在遍歷的時候,使用for(int i = 0; i < size; i++) 來遍歷,其速度比使用Iterator快(接口上是這麼寫的)。可是筆者看源碼的時候,Iterator裏使用的也是i++,這種遍歷,無非是增長了fail-fast判斷,估計就是這個致使了性能的差距,可是沒有LinkedList這麼大。筆者循環了 1000 * 1000 次,貼出比較結果,僅供參考,有興趣的朋友們能夠試一試,循環次數越多越明顯:函數

----------now is arraylist----------
使用Iterator迭代一共花了19ms時間
使用for-i迭代一共花了9ms時間
----------now is linkedList----------
使用Iterator迭代一共花了17ms時間
使用for-i迭代一共花了434ms時間

而其繼承的AbstractList主要給ArrayList提供了諸如addgetsetremove的集合方法。oop


3.2 ArrayList的成員變量

//初始化默認容量
private static final int DEFAULT_CAPACITY = 10;
// 空對象數組
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默認容量的空對象數組
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 實際存儲對象的數組
transient Object[] elementData;
// 存儲的數量
private int size;
// 數組能申請的最大數量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

特別說明一下: EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA是爲了在調用構造方法的時候,給elementData數組初始化,當elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的時候,當ArrayList第一次插入元素的時候,它的數組大小將會被初始化爲DEFAULT_CAPACITY。而EMPTY_ELEMENTDATA能夠理解爲初始化的時候size=0,下面讓咱們看下構造方法,來更加清楚的理解。性能


3.3 ArrayList的構造方法

3.3.1 ArrayList()

public ArrayList() {
  this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

當調用默認構造函數的時候,給elementData指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA。學習


3.3.2 ArrayList(int initialCapacity)

public ArrayList(int initialCapacity) {
  // 當 initialCapacity > 0 時,初始化對應大小的數組
  if (initialCapacity > 0) {
    this.elementData = new Object[initialCapacity];
  // 爲 0 時,用指向EMPTY_ELEMENTDATA
  } else if (initialCapacity == 0) {
    this.elementData = EMPTY_ELEMENTDATA;
  } else {
    throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
  }
}

這裏當initialCapacity=0的時候,就是上述提到的狀況。this


3.3.3 ArrayList(Collection<? extends E> c)

public ArrayList(Collection<? extends E> c) {
  elementData = c.toArray();
  if ((size = elementData.length) != 0) {
    // c.toArray不返回Object[]的時候,則進行數組拷貝
    if (elementData.getClass() != Object[].class)
      elementData = Arrays.copyOf(elementData, size, Object[].class);
  } else {
    // 若是爲空,則指向EMPTY_ELEMENTDATA
    this.elementData = EMPTY_ELEMENTDATA;
  }
}

3.4 ArrayList的重要方法

3.4.1 get(int index)

public E get(int index) {
  rangeCheck(index);
  return elementData(index);
}

E elementData(int index) {
  return (E) elementData[index];
}

get方法很簡單,就是先檢查index範圍是否正確,正確的話從數組裏取出元素。3d

private void rangeCheck(int index) {
  // 若是index 大於 存儲的個數,則拋出異常
  if (index >= size)
    // outOfBoundsMsg裏面就是簡單的字符串拼接。
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

這裏值得一提的是:這裏只判斷了index >= size的狀況,對於index < 0的狀況沒有判斷,是由於在獲取數組值的時候,若是爲負數會拋出ArrayIndexOutOfBoundsException異常。


3.4.2 add(E e)

在看源碼以前,咱們先思考一個問題,往數組裏添加元素的時候要注意什麼:

  1. 對於剛初始化的數組,要初始化它的大小
  2. 判斷數組大小是否足夠,若是不夠大,擴容
  3. 對於擴容要判斷是否到達數組的最大數量

知道這些須要考慮以後,咱們再來看看它的代碼:

public boolean add(E e) {
  //對上述的3個前提進行判斷
  ensureCapacityInternal(size + 1);
  //賦值,而後指針走到下一個空位
  elementData[size++] = e;
  return true;
}

咱們接着來看ensureCapacityInternal()的方法代碼:

private void ensureCapacityInternal(int minCapacity) {
  // 上述狀況一:初始化數組的大小
  if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    // 取minCapacity和DEFAULT_CAPACITY中較大的那個
    minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
  }
  // 檢查有沒有擴容的必要
  ensureExplicitCapacity(minCapacity);
}

ensureCapacityInternal()方法的做用就是對構造方法初始化的數組進行處理。

再來看一下ensureExplicitCapacity():

private void ensureExplicitCapacity(int minCapacity) {
  // 修改次數的計數器(在AbstractList中定義的)
  modCount++;
  // 若是須要的空間大小 > 當前數組的長度,則進行擴容
  if (minCapacity - elementData.length > 0)
    grow(minCapacity);
}

ensureExplicitCapacity()檢查是否須要擴容。

private void grow(int minCapacity) {
  // 記錄舊的length
  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)
    throw new OutOfMemoryError();
  // 須要的最小容量 > 數組最大的長度,則取Integer的最大值,不然取數組最大長度
  return (minCapacity > MAX_ARRAY_SIZE) ?
    Integer.MAX_VALUE :
  MAX_ARRAY_SIZE;
}

最後的grow()擴容就是判斷有沒有超過數組的最大長度,以及對應的處理。


3.4.3 remove(int index)

public E remove(int index) {
  rangeCheck(index);
  // 修改計數器
  modCount++;
  // 記錄舊值,返回
  E oldValue = elementData(index);
  // 計算要往前移動的元素個數
  int numMoved = size - index - 1;
  //個數大於0,進行拷貝,從index+1開始,拷貝numMoved個,拷貝起始位置是index
  if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,
                     numMoved);
  // 設置爲null,以便GC
  elementData[--size] = null;
  return oldValue;
}

對於被刪除的元素,其後面的元素須要往前移。


3.4.4 remove(Object o)

public boolean remove(Object o) {
  // 判斷o爲null,loop遍歷找到爲null的元素
  if (o == null) {
    for (int index = 0; index < size; index++)
      if (elementData[index] == null) {
        fastRemove(index);
        return true;
      }
  // 不爲null
  } else {
    for (int index = 0; index < size; index++)
      if (o.equals(elementData[index])) {
        fastRemove(index);
        return true;
      }
  }
  return false;
}

// 與上面的remove(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;
}

3.4.5 set(int index, E element)

public E set(int index, E element) {
  rangeCheck(index);
  // 記錄舊的值
  E oldValue = elementData(index);
  //在原位置設置新的值
  elementData[index] = element;
  return oldValue;
}

設置index位置的元素值爲element,返回該位置的原來的值


3.4.6 addAll(Collection<? extends E> c)

public boolean addAll(Collection<? extends E> c) {
  Object[] a = c.toArray();
  int numNew = a.length;
  // 對於新的最小長度進行判斷處理
  ensureCapacityInternal(size + numNew);
  //將a數組,從index-0開始,拷貝numNew個,到elementData的size位置
  System.arraycopy(a, 0, elementData, size, numNew);
  //將size增長numNew個
  size += numNew;
  return numNew != 0;
}

4、總結

ArrayList在隨機訪問的時候,數組的結構致使訪問效率比較高,可是在指定位置插入,以及刪除的時候,須要移動大量的元素,致使效率低下,在使用的時候要根據場景特色來選擇,另外注意循環訪問的方式選擇。最後謝謝各位園友觀看,若是有描述不對的地方歡迎指正,與你們共同進步!

相關文章
相關標籤/搜索