容器學習之01ArrayList

1. 簡介

ArrayList 我們幾乎每天都會使用到,但關於ArrayList的細節我們是否真正關注過?本文大家一起通過源碼來重新認識ArrayList。
ArrayList顧名思義,其內部是用數組來存放數據。在初始化時,會爲我們生成一個默認大小的數組。往容器裏添加數據,其實就是在往數組裏add。
在ArrayList的類註釋中明確寫道:

  • 允許 put null 值,會自動擴容
  • size、isEmpty、get、set、add 等方法時間複雜度都是 O (1);
  • 是非線程安全的,多線程情況下,推薦使用線程安全類:Collections#synchronizedList;
  • 增強 for 循環,或者使用迭代器迭代過程中,如果數組大小被改變,會快速失敗,拋出異常。

而ArrayList類圖如下:
arrayList類圖

2. 簡單事例

public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

上面中,並沒有使用大家熟悉的for循環,而是使用自帶的iterator,通過while循環輸出list內容。

3. 源碼分析

3.1 構造函數

ArrayList構造函數有三種:無參數直接初始化、指定大小初始化、指定初始數據初始化,源碼及註釋如下:

  private static final Object[] 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() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

   //根據傳入的集合初始化,元素順序是按照,傳入的集合順序指定的
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

除了源碼的中文註釋,還需要注意: ArrayList 無參構造器初始化時,默認大小是空數組,並不是大家常說的 10,10 是在第一次 add 的時候擴容的數組值。

3.2 新增和擴容實現

新增就是往數組中添加元素,主要分成兩步:

  • 判斷是否需要擴容,如果需要執行擴容操作;
  • 直接賦值。
public boolean add(E e) {
  //確保數組大小是否足夠,不夠執行擴容,size 爲當前數組的大小
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  //直接賦值,線程不安全的
  elementData[size++] = e;
  return true;
}

擴容代碼如下:

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.5 倍;

  • ArrayList 中的數組的最大值是 Integer.MAX_VALUE,超過這個值,JVM 就不會給數組分配內存空間了。

  • 新增時,並沒有對值進行嚴格的校驗,所以 ArrayList 是允許 null 值的。

從新增和擴容源碼中,下面這點值得我們借鑑:

  • 源碼在擴容的時候,有數組大小溢出意識,就是說擴容後數組的大小下界不能小於 0,上界不能大於 Integer 的最大值,這種意識我們可以學習。
  • 擴容完成之後,賦值是非常簡單的,直接往數組上添加元素即可:elementData [size++] = e。也正是通過這種簡單賦值,沒有任何鎖控制,所以這裏的操作是線程不安全的,對於新增和擴容的實現,畫了一個動圖,如下:
    擴容動圖
3.3 擴容的本質

數組的擴容,最底層是通過public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);這段代碼實現的,它被native 修飾,表示這是JVM提過我們的放法,實現是由JVM來實現,我們不需要關心。

3.4 刪除

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

我們需要注意的是:

  • 新增的時候是沒有對 null 進行校驗的,所以刪除的時候也是允許刪除 null 值的;
  • 找到值在數組中的索引位置,是通過 equals 來判斷的,這說明如果是自定義類型,則需要我們去確認自定義類型的equals 放法。
    下面一個gif演示其過程:
    刪除動圖
3.5 迭代器

如果要自己實現迭代器,實現 java.util.Iterator 類就好了,ArrayList 也是這樣做的,我們來看下迭代器的幾個總要的參數:

// 迭代過程中,下一個元素的位置,默認從 0 開始。
int cursor;
 // 新增場景:表示上一次迭代過程中,索引的位置;刪除場景:爲 -1。
int lastRet = -1;
// expectedModCount 表示迭代過程中,期望的版本號;modCount 表示數組實際的版本號。
int expectedModCount = modCount;

迭代器一般來說有三個方法:

  • boolean hasNext(); 還有沒有值可以迭代,返回bool
  • E next() 如果有值可以迭代,迭代的值是多少,返回Object
  • void remove() 刪除當前迭代的值

下面是ArrayList迭代器源碼:

 private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        public boolean hasNext() {
        	//cursor 表示下一個元素的位置,size 表示實際大小,
        	//如果兩者相等,說明已經沒有元素可以迭代了,如果不等,說明還可以迭代
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
		  //迭代過程中,判斷版本號有無被修改,有被修改,拋 ConcurrentModificationException 異常
		  checkForComodification();
		  //本次迭代過程中,元素的索引位置
		  int i = cursor;
		  if (i >= size)
		    throw new NoSuchElementException();
		  Object[] elementData = ArrayList.this.elementData;
		  if (i >= elementData.length)
		    throw new ConcurrentModificationException();
		  // 下一次迭代時,元素的位置,爲下一次迭代做準備
		  cursor = i + 1;
		  // 返回元素值
		  return (E) elementData[lastRet = i];
		}
		// 版本號比較
		final void checkForComodification() {
		  if (modCount != expectedModCount)
		    throw new ConcurrentModificationException();
		}

        public void remove() {
          // 如果上一次操作時,數組的位置已經小於 0 了,說明數組已經被刪除完了
  			if (lastRet < 0)
		    	throw new IllegalStateException();
			  //迭代過程中,判斷版本號有無被修改,
			  //有被修改,拋 ConcurrentModificationException 異常
			  checkForComodification();
		  try {
		    ArrayList.this.remove(lastRet);
		    cursor = lastRet;
		    // -1 表示元素已經被刪除,這裏也防止重複刪除
		    lastRet = -1;
		    // 刪除元素時 modCount 的值已經發生變化,在此賦值給 expectedModCount
		    // 這樣下次迭代時,兩者的值是一致的了
		    expectedModCount = modCount;
		  } catch (IndexOutOfBoundsException ex) {
		    throw new ConcurrentModificationException();
		  }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

remove這裏我們需要注意的兩點是:

  • lastRet = -1 的操作目的,是防止重複刪除操作
  • 刪除元素成功,數組當前 modCount 就會發生變化,這裏會把 expectedModCount 重新賦值,下次迭代時兩者的值就會一致了

4. 總結

  • 從我們上面新增或刪除方法的源碼解析,對數組元素的操作,只需要根據數組索引,直接新增和刪除,所以時間複雜度是 O (1)。
  • ArrayList是線程不安全的,最根本原因是因爲 ArrayList 自身的 elementData、size、modConut 在進行各種操作時,都沒有加鎖,而且這些變量的類型並非是可見(volatile)的,所以如果多個線程對這些變量進行操作時,可能會有值被覆蓋的情況。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章