DEBUG ArrayList

noneplus發表於2020-07-18

1,ArrayList面試必問

  • 說說ArrayList和LinkedList的區別?

    ArrayList基於陣列實現,LinkedList基於連結串列實現,不同的資料結構決定了ArrayList查詢效率比較高,而LinkedList插入刪除效率比較高,反過來就比較慢了。

  • ArrayList預設初始容量為多少?按照幾倍來擴容?

    10,1.5倍。

  • 說說陣列擴容的原理?

    ArrayList擴容呼叫的是Array.copyof函式,把老陣列遍歷賦值給新陣列返回。

  • 說說ArrayList常見方法的時間複雜度?

    • get方法通過下標獲取元素,時間複雜度為O(1)
    • add方法直接新增會新增到集合的尾部,時間複雜度為O(1)
    • add方法通過下標新增到非尾部會引起陣列的批量移動,時間複雜度為O(n),否則為O(1)
    • remove方法通過下標刪除非尾部元素引起陣列批量移動,時間複雜度為O(n),反之則為O(1)
    • remove方法根據物件刪除需要遍歷集合,時間複雜度為O(n),如果刪除的為非尾部元素,會使陣列批量移動,時間複雜度為O(n^2)

    總之,通過下標操作的時間複雜度為O(1),如果觸發了陣列的批量移動,時間複雜度為O(n),如果通過物件操作需要遍歷集合,時間複雜度已經為O(n),若同時觸發了陣列的移動,時間複雜度為O(n^2).

  • ArrayList和vector的區別

    • 最大的區別在於執行緒是否安全
    • 其次Vector是兩倍擴容
    • 最後就是在不指定大小的情況下,ArrayList容量初始化是在新增元素的時候,而Vector有一個無參構造器直接初始化為10

2,Debug ArrayList原始碼

由於1.7和1.8幾乎沒什麼變化,本文以jdk1.8為例。

2.1 用Debug分析一個元素是如何add進ArrayList

編寫測試用例,打上斷點:

image-20200718134052340

先分析建構函式如何初始化,關鍵步驟如下:

image-20200718134136152

elementData是ArraList底層陣列的實現,(ps:hashMap陣列使用table命名)

image-20200718134210282

DEFAULTCAPACITY_EMPTY_ELEMENTDATA表示預設的空陣列,也就是說ArrayList在建構函式初始化時並不會進行底層陣列的初始化。

image-20200718134341634

給元素的新增打上斷點,分析過程:

image-20200718134636053

進入add方法內部:

public boolean add(E e) {
		//確保內部容量,在元素新增進來前可能要進行擴容操作,size初始化為0,表示集合的長度
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //新增元素,size自增
        elementData[size++] = e;
        return true;
    }

進入ensureCapacityInternal方法內部:此時elementData為空,size+1=minCapacity=1

ensureExplicitCapacity:確保明確的能力

image-20200718135014136

計算容量,calculateCapacity方法:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
	//判斷陣列是否為空,若為空,返回預設容量和最小容量的最大值,若不為空,返回最小容量
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

DEFAULT_CAPACITY預設容量為10:

image-20200718135315367

繼續分析,進入:

ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));此時引數為10,也就是ArrayList的預設容量
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;   //集合的修改次數

    //如果最小容量減去陣列長度大於0,進行擴容,此時最小容量為10,陣列長度為0
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

核心擴容函式grow:(ps:HashMap中擴容函式為resize)

private void grow(int minCapacity) {
    //oldCapacity:舊陣列容量
    int oldCapacity = elementData.length;
   
   //新容量等於舊容量加上舊容量的一半,>>1相當於除以2(ArrayList擴容是1.5倍)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    //新容量小於最小容量,則賦值為最小容量,此時newCapacity等於0,minCapacity為10
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
        
    //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    //賦值給新陣列
    elementData = Arrays.copyOf(elementData, newCapacity);
}

陣列複製Arrays.copyOf:

image-20200718142335257

image-20200718142450568

2.2 用Debug分析如何通過陣列下標獲取ArrayList元素

打上斷點,debug:

image-20200718143338620

首先進行範圍檢查,而後返回元素

image-20200718143422580

image-20200718143458143

2.3 用Debug分析如何通過陣列下標刪除一個元素

打上斷點:

image-20200718143953768

進入remove方法內部,

 public E remove(int index) {
 		//下標範圍檢查
        rangeCheck(index);
		//修改次數自增
        modCount++;
        //保留當前刪除元素的值,稍後返回
        E oldValue = elementData(index);
		//需要移動元素的個數
        int numMoved = size - index - 1;
        if (numMoved > 0)
        //底層使用native方法,debug進不去。native方法:java呼叫其他語言的介面
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //最後一位置空                     
        elementData[--size] = null; // clear to let GC do its work
		//返回刪除元素的值
        return oldValue;
    }

2.4 用Debug分析如何通過物件刪除一個元素

image-20200718144826001

進入remove方法:

public boolean remove(Object o) {
//如果物件為空,則遍歷ArrayList集合
        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方法:

 private void fastRemove(int index) {
        modCount++;
        
        //numMoved:需要移動陣列的個數
        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 wrk
    }

2.5 用Debug分析向陣列中間新增元素

image-20200718150021832

進入add方法

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

關於System.arraycopy時間複雜度問題,在新增或者刪除最後一個元素的時候不會觸發陣列的複製機制,時間複雜度為O(1),若是新增到陣列中間,由於會觸發陣列的複製,時間複雜度為O(n)。對於刪除元素同樣,根據陣列下標刪除的情況下,刪除尾部元素是不會觸發陣列的擴容機制的,若刪除中間的元素,同樣會觸發陣列的複製。若根據物件刪除元素,由於本身遍歷到物件的時間複雜度為O(n),刪除元素後再對陣列進行重組,所以時間複雜度為O(n^2)。