深入Java集合ArrayList的源碼解析

現在由大惡人付有傑來從增刪改查幾個角度輕度解析ArrayList的源碼

首先ArrayList的底層數據結構非常簡單,就是一個數組。
在這裏插入圖片描述
從源碼第115行我們可以得出信息,他的默認數組長度是10。

/**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

那麼我們經常調用的size方法是什麼呢?
源碼第281行,142行

 /**
     * Returns the number of elements in this list.
     *返回鏈表中元素的個數
     * @return the number of elements in this list
     */
    public int size() {
        return size;
    }
 /**
     * The size of the ArrayList (the number of elements it contains).
     *同上
     * @serial
     */
    private int size;

另外,還有一個關鍵的屬性:

//modCount 統計當前數組被修改的版本次數,數組結構有變動,就會 +1
 protected transient int modCount = 0;

以上表達的意思就是說,ArrayList的默認大小是10,內部記錄有自己被修改的次數,和鏈表中有效的元素。所謂有效的元素就是你自己添加的元素。

1.構造方法

有三種構造方法:
在這裏插入圖片描述
1.指定大小初始化

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

清晰明瞭哈,如果指定的大小是大於0的,那麼就用這個數字初始化,否則就初始一個空的數組。如果是非法輸入(<0),就會拋出IllegalArgumentException異常。
2.無參構造函數初始化

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

我們可以看到,無參構造 函數並不是一來就 初始化了10個長的數組,而是初始化了一個空的數組。這樣能夠省點空間吧。面試官問起來了,初始化的大小是10碼?絕對不是哈,是0。
3.指定數據初始化

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

這個代碼的意思就是,凡是繼承於Collection的,爺都能初始化。List接口繼承自Collection的。
演示一下2種姿勢

		 List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        ArrayList<Integer> arrayList = new ArrayList<>(list);
        ArrayList<Integer> integers = new ArrayList<>(Arrays.asList(2, 4, 5, 6, 7, 8));

是不是很方便,如果你不知道這個方法,你還要手動去add 1 2 3 4 5.

2.新增和擴容實現

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

  • 判斷是否需要擴容,如果需要執行擴容操作;
  • 直接賦值。
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
 public void add(int index, E element) {
 //判斷索引是不是合法的
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

我們常用時第一種,第二種是在指定位置添加,把原來位置 的擠到後面去。後面的所有元素都要讓一步,性能消耗會很大。
在添加元素之前,總是有一個

  //確保數組大小是否足夠,不夠執行擴容,size 爲當前數組的大小
 ensureCapacityInternal(size + 1);

我們仔細想一想是吧,你添加元素,size就加1,所以就判斷size+1是否滿足。
ensureCapacityInternal(size + 1);做了很多事情。我把相關的代碼都複製過來。

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
   private void ensureExplicitCapacity(int minCapacity) {
   //記錄數組被修改(添加一個元素,肯定被 修改了呀)
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //原來老舊的容量除以2,加上老的容量。實錘了!!1.5倍速擴容
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果擴容了,還是不夠用,那麼就用你聲明的值。
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            //如果擴容了,大於Integer.MaxValue 就用Inter.maxValue
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        //將數據從原來的數組複製到新的數組上
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

上面代碼,關鍵地方我都給出了中文解釋。希望你能明白。
所以我們可以總結一下:

  • 擴容的規則並不是翻倍,是原來容量大小 + 容量大小的一半,直白來說,擴容後的大小是原來容量的 1.5 倍;
int newCapacity = oldCapacity + (oldCapacity >> 1);
  • ArrayList 中的數組的最大值是 Integer.MAX_VALUE,超過這個值,JVM 就不會給數組分配內存空間了。
if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
  • 新增時,並沒有對值進行嚴格的校驗,所以 ArrayList 是允許 null 值的。
  • 源碼在擴容的時候,有數組大小溢出意識,就是說擴容後數組的大小下界不能小於 0,上界不能大於 Integer
    的最大值,這種意識我們可以學習。
private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);

擴容的本質:

擴容的本質就是新開了一個擴容的數組,然後把原來數組的元素批量賦值過去,最後修改內部的elementData引用。

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        //複製了原有 數組元素,然後修改elementData
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

** Arrays.copyOf是調用的 System.arraycopy,後者是本地方法**

 public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

3.刪除

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

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

我們需要注意的兩點是:

  • 新增的時候是沒有對 null 進行校驗的,所以刪除的時候也是允許刪除 null 值的;
  • 找到值在數組中的索引位置,是通過 equals 來判斷的,如果數組元素不是基本類型,需要我們關注 equals 的具體實現。(這個和 == 的區別,不懂自己去百度哈)
    然後看看裏面的fastRemove(源碼544行)
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;
}

從源碼中,我們可以看出,某一個元素被刪除後,爲了維護數組結構,我們都會把數組後面的元素往前移動(所以說,數組的刪除性能開銷真的很大)

4.迭代

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

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

在這裏插入圖片描述
迭代器一般來說有三個方法:

  • hasNext 還有沒有值可以迭代
  • next 如果有值可以迭代,迭代的值是多少
  • remove 刪除當前迭代的值
public boolean hasNext() {
  return cursor != size;//cursor 表示下一個元素的位置,size 表示實際大小,如果兩者相等,說明已經沒有元素可以迭代了,如果不等,說明還可以迭代
}
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();
}

從源碼中可以看到,next 方法就幹了兩件事情,第一是檢驗能不能繼續迭代,第二是找到迭代的值,併爲下一次迭代做準備(cursor+1)。

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();
  }
}
  • 刪除元素成功,數組當前 modCount 就會發生變化,這裏會把 expectedModCount 重新賦值,下次迭代時兩者的值就會一致了

其他:

都說數組的添加元素 的時間複雜度是O(1),真的如此嗎?

如果我們直接調用Add(x)的方法,且數組容量足夠這個數組掛在後面,那麼時間複雜度就是1,如果觸發了擴容機制,那麼就是O(N),在使用add(index,e)的時候,時間複雜度一般來說不是O1,因爲要移動索引 後面的元素。

什麼是falilFast的機制?
在遍歷過程中,如果數據被修改,就會報錯。

 Iterator<Integer> iterator = arrayList.iterator();
        new Thread(new Runnable() {
            @Override
            public void run() {
                arrayList.remove(2);
            }
        }).start();
        while (iterator.hasNext()){
            Thread.sleep(50);
            System.out.println(iterator.next());
        }
        //同上
        for (Integer integer : arrayList) {
            System.out.println(integer);
        }

在這裏插入圖片描述
上面提到,遍歷的時候會記錄modCount的值,如果和自己期望的不一樣,就會報錯。
fail-fast解決辦法
方案一:在遍歷過程中所有涉及到改變modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,這樣就可以解決。但是不推薦,因爲增刪造成的同步鎖可能會阻塞遍歷操作。
方案二:使用CopyOnWriteArrayList來替換ArrayList。推薦使用該方案。

數組初始化,被加入一個值後,如果我使用 addAll 方法,一下子加入 15 個值,那麼最終數組的大小是多少?
分析:在加入一個元素的時候,數組被初始化成10個,然後一下子加入15個,那麼就會觸發擴容,在初次擴容後,大小變成了15(1.5倍速度擴容),發現 還是不夠用,就會使用1+15這個值作爲容量。所以答案是16.
再貼一遍源碼

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //原來老舊的容量除以2,加上老的容量。實錘了!!1.5倍速擴容
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果擴容了,還是不夠用,那麼就用你聲明的值。
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            //如果擴容了,大於Integer.MaxValue 就用Inter.maxValue
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        //將數據從原來的數組複製到新的數組上
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

現在我有一個很大的數組需要拷貝,原數組大小是 5k,請問如何快速拷貝?
因爲原數組比較大,如果新建新數組的時候,不指定數組大小的話,就會頻繁擴容,頻繁擴容就會有大量拷貝的工作,造成拷貝的性能低下,所以回答說新建數組時,指定新數組的大小爲 5k 即可。
所以大惡人付有傑建議,平常自己心知肚明的時候,自己手動指定大小。

還有ArrayList刪不乾淨的問題:

List<Integer> list = new ArrayList<>(Arrays.asList(1,2,2,2,2,3));
        for(int i = 0;i<list.size();i++){
            if(list.get(i).equals(2)){
                list.remove(i);
            }
        }
        System.out.println(list.toString());

代碼輸出:
在這裏插入圖片描述
兄弟們自己去想吧。

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