1. 程式人生 > 實用技巧 >面試官:你說你精通原始碼,那你知道ArrayList 原始碼的設計思路嗎?

面試官:你說你精通原始碼,那你知道ArrayList 原始碼的設計思路嗎?

Arraylist原始碼分析

ArrayList 我們幾乎每天都會使用到,但是通常情況下我們只是知道如何去使用,至於其內部是怎麼實現的我們不關心,但是有些時候面試官就喜歡問與ArrayList 的原始碼相關的問題,今天我們就來看看和ArrayList 原始碼相關的問題。

一:整體架構

1.1、ArrayList 結構

ArrayList 整體架構比較簡單,就是一個數組結構,比較簡單,如下圖:

圖中展示是長度為 n 的陣列,index 表示陣列的下標,從 0 開始計數,elementData 表示陣列本身,原始碼中除了這兩個概念,還有以下三個基本概念:

  1. DEFAULT_CAPACITY ,表示陣列的初始大小,預設是 10;
  2. size ,當前陣列的大小,沒有使用 volatile 修飾,非執行緒安全的;
  3. modCount ,統計當前陣列被修改的版本次數;

1.2、ArrayList 類註釋

看原始碼,首先要看類註釋,我們看看類註釋上面都說了什麼,部分截圖如下圖所示:

類註釋主要講了以下四點

  1. 允許put null 值,會自動擴容 ;
  2. size、isEmpty、get、set、add 等方法時間複雜度都是 O (1) ;
  3. 是非執行緒安全的,多執行緒情況下,推薦使用執行緒安全類:Collections#synchronizedList ;
  4. 增強 for 迴圈,或者使用迭代器迭代過程中,如果陣列大小被改變,會快速失敗,丟擲異常

二:原始碼解析

2.1、初始化

ArrayList 有三種初始化辦法:無引數直接初始化、指定大小初始化、指定初始資料初始化,原始碼如下:

// 無引數直接初始化,陣列大小為空
public ArrayList() {
	this.elementData = DEFAULTCAPACITY_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);
   }
}

// 指定初始資料初始化
public ArrayList(Collection<? extends E> c) {
	elementData = c.toArray();
    if ((size = elementData.length) != 0) {
    	// c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class) {
        elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
      // replace with empty array.
      this.elementData = EMPTY_ELEMENTDATA;
   }
}

注意: ArrayList 無參構造器初始化時,預設大小是空陣列,並不是大家常說的 10,10 是在第一次 add 的時候擴容的陣列值。

2.2、新增和擴容實現

新增方法主要分成兩步:首先判斷陣列是否需要擴容,如果需要,執行擴容操作,否則,直接賦值。新增方法原始碼如下,其中ensureCapacityInternal()方法就是擴容操作。

public boolean add(E e) {
  //確保陣列大小是否足夠,不夠執行擴容,size 為當前陣列的大小
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  //直接賦值,執行緒不安全的
  elementData[size++] = e;
  return true;
}

擴容(ensureCapacityInternal)的原始碼:

private void ensureCapacityInternal(int minCapacity) {
  // 如果初始化陣列大小時,有給定初始值,以給定的大小為準,不走 if 邏輯
  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) {
  int oldCapacity = elementData.length;
  // oldCapacity >> 1 是把 oldCapacity 除以 2 的意思
  int newCapacity = oldCapacity + (oldCapacity >> 1);

  // 如果擴容後的值 < 我們的期望值,擴容後的值就等於我們的期望值
  if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;

  // 如果擴容後的值 > jvm 所能分配的陣列的最大值,那麼就用 Integer 的最大值
  if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);

  // 通過複製進行擴容
  elementData = Arrays.copyOf(elementData, newCapacity);
}

從擴容的原始碼我們可以看出:

  1. 擴容的規則並不是翻倍,而是原來容量的 1.5 倍 ;
  2. ArrayList 中的陣列容量的最大值是 Integer.MAX_VALUE,超過這個值,JVM 就不會給陣列分配記憶體空間了 ;

2.3、刪除

ArrayList 刪除元素有很多種方式,比如根據陣列索引刪除、根據值刪除或批量刪除等等,原理和思路都差不多,我們選取根據值刪除方式來進行原始碼說明:

public boolean remove(Object o) {
  // 如果要刪除的值是 null,找到第一個值是 null 的刪除
  if (o == null) {
    for (int index = 0; index < size; index++)
      if (elementData[index] == null) {
        fastRemove(index);
        return true;
      }
  } else {
    // 如果要刪除的值不為 null,找到第一個和要刪除的值相等的刪除
    for (int index = 0; index < size; index++)
      // 這裡是根據  equals 來判斷值相等的,相等後再根據索引位置進行刪除
      if (o.equals(elementData[index])) {
        fastRemove(index);
        return true;
      }
  }
  return false;
}

從上面的原始碼中我們可以看出:

  1. 由於新增的時候沒有對 null 值做出判斷,所以是可以刪除 null 值的 ;
  2. 找到值在陣列中的索引位置,是通過 equals 來判斷的,如果陣列元素不是基本型別,我們需要關注 equals 的具體實現;

刪除的具體邏輯是在fastRemove()中實現的,原始碼如下所示

private void fastRemove(int index) {
  // 記錄陣列的結構要發生變動了
  modCount++;
  // numMoved 表示刪除 index 位置的元素後,需要從 index 後移動多少個元素到前面去
  // 減 1 的原因,是因為 size 從 1 開始算起,index 從 0開始算起
  int numMoved = size - index - 1;
  if (numMoved > 0)
    // 從 index +1 位置開始被拷貝,拷貝的起始位置是 index,長度是 numMoved
    System.arraycopy(elementData, index+1, elementData, index, numMoved);
  //陣列最後一個位置賦值 null,幫助 GC
  elementData[--size] = null;
}

2.4、複雜度分析

操作 時間複雜度
get() 根據下標直接查詢 O(1)
add(E e) 直接尾部新增 O(1)
add(int index, E e) 插入後元素需要往後移動一個單位 O(n)
remove(E e) 刪除後元素需要往前移動一個單位 O(n)

2.5、執行緒安全

我們需要強調的是,只有當 ArrayList 作為共享變數時,才會有執行緒安全問題,當 ArrayList 是方法內的區域性變數時,是沒有執行緒安全的問題的。

ArrayList 有執行緒安全問題的本質,是因為 ArrayList 自身的 elementData、size、modConut 在進行各種操作時,都沒有加鎖,而且這些變數的型別並非是可見(volatile)的,所以如果多個執行緒對這些變數進行操作時,可能會有值被覆蓋的情況。

類註釋中推薦我們使用 Collections#synchronizedList 來保證執行緒安全,SynchronizedList 是通過在每個方法上面加上鎖來實現,雖然實現了執行緒安全,但是效能大大降低。

三:總結

從上面 ArrayList 的部分原始碼的分析中,我們可以發現 ArrayList 底層其實就是圍繞陣列結構,各個 API 都是對陣列的操作進行封裝,讓使用者無需感知底層實現,只需關注如何使用即可。

最後

感謝你看到這裡,看完有什麼的不懂的可以在評論區問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!