簡單的解剖了下ArrayList

簡介

ArrayList是一個可動態調整數組大小的集合類,其類圖關係如下:
在這裏插入圖片描述

  • List:聲明是一個有序的集合,可以控制元素位置並索引訪問。

  • RandomAccess:聲明支持快速隨機訪問的標記接口,常用於列表類實現。該接口的主要目的是允許通用算法更改其行爲,即必要時選擇更好的算法進行性能上的提高,實現了該接口的列表使用for遍歷比迭代器Iterator遍歷效率高。
    RandomAccess.png

  • Serializable:啓用類的可序列化特性。

  • Cloneable:聲明類是可克隆的,且調用clone()方法時不會拋出CloneNotSupportedException

  • AbstractList:提供了List接口的基本實現,並儘可能的減少List接口"隨機訪問"數據存儲支持的工作

ArrayList核心源碼解析

從屬性與方位兩個維度進行源碼解析並總結特點。

屬性解析

DEFAULT_CAPACITY:默認容量10,用於構造函數初始化與容量運算。
EMPTY_ELEMENTDATA:共享的空數組,調用ArrayList有參構造函數參數容量值爲0(即一般考慮不再進行容量擴展)時賦給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) {
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

DEFAULTCAPACITY_EMPTY_ELEMENTDATA:共享的空數組,與EMPTY_ELEMENTDATA區別在於該數組是用來容量運算的,調用ArrayList無參構造函數時會把該對象賦給elementData,添加元素時再重新計算擴容,所以一般建議使用有參構造函數賦予原始容量。

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

elementData:存儲ArrayList的元素的數組緩衝區。
sizeArrayList包含的元素數量,elementData數組的元素數量。
MAX_ARRAY_SIZE:分配的最大數組大小,值爲Integer.MAX - 8

方法解析

add(E)添加元素

/**
 * (1) 數組末尾添加元素 
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 容量值自增並將元素附加到數組末尾
    elementData[size++] = e;
    return true;
}

/**
 * (2) 確保內部的容量能滿足所需最小容量minCapacity
 */
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

/**
 * (3) 根據數組所需的最小容量minCapacity進行容量計算
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 若元素數組爲引用的空數組,則返回默認容量(10)與minCapacity之間的最大值,不爲空則返回minCapacity
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

/**
 * (4) 根據數組所需的最小容量minCapacity確保精確的容量
 */
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 判斷添加元素後的元素數目是否大於數組長度,true則進行數組擴容,false則完成元素添加
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

/**
 * (5) 根據數組所需的最小容量minCapacity判斷是否擴容
 */
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 判斷添加元素後的元素數目是否大於數組長度,true則進行擴容,false則完成元素添加
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}


/**
 * (6) 重新建一個至少可以容納最小容量minCapacity的數組並進行數組元素拷貝,消耗大
 */
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 若所需最小容量minCapacity大於舊容量oldCapacity+oldCapacity右移1值,則新容量爲minCapacity,反正新容量爲舊容量運算值
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 大容量數組,一般不會調用到
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 新建一個長度爲newCapacity的數組並將舊數組元素負責過來
    elementData = Arrays.copyOf(elementData, newCapacity);
}

/**
 * 大容量計算,一般不會調用到
 */
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

以下重新整理一下新增的的步驟:

  1. add()數組末尾添加元素
  2. ensureCapacityInternal()確保內部的容量能滿足所需最小容量minCapacity
  3. calculateCapacity()根據數組所需的最小容量minCapacity進行容量計算
  4. ensureExplicitCapacity()根據數組所需的最小容量minCapacity確保精確的容量
  5. ensureExplicitCapacity()根據數組所需的最小容量minCapacity判斷是否擴容,若需要則進行步驟6
  6. grow()重新建一個至少可以容納最小容量minCapacity的數組並進行數組元素拷貝,消耗大,所以建議一般使用有參構建函數創建列表時設置好容量

由上述流程可以看出,ArrayListadd(E e)方法在容量足以確保的情況下效率是很高的,直接將新元素賦予數組元素的末尾下標+1即可,複雜度僅爲O(1)。

add(int, E)增元素

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()方法,該方法把elementData數組中的index位置起的size-index個元素(即index下標後的元素)複製到下標index+1,然後再把新的元素element賦到index下標位置。由於需要進行元素的位置逐個後移,所以性能耗費大,時間複雜度爲O(n),n爲指定位置後的元素數目。
如在非末尾位置插入元素的操作較多,選擇LinkedList效果會比ArrayList更好。

addAll(Collection<? extends E>)添加元素

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

由上源碼可以看到當添加集合元素時,也是需要進行數組拷貝的,不過是直接拷貝到列表數組末尾,時間複雜度由集合元素數目而定,即爲O(n)。

remove(Object)刪元素

public boolean remove(Object o) {
    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;
}

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; // clear to let GC do its work
}

雖然刪除元素的主要方面命名爲fastRemove(),但從其代碼依然可以看出這方法並不fast,指定位置刪除元素後還要進行元素前移,性能耗費與指定位置添加差別不大,時間複雜度爲O(n),n爲指定位置後的元素數目。
如刪除元素的操作較多,選擇LinkedList效果會比ArrayList更好。

set(int, E)改元素

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

替換指定下標數組元素,複雜度爲O(1),效率高。

get(int)查元素

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

根據下標獲取數組元素,複雜度爲O(1),效率高。

總結

ArrayList有以下特點:

  • 添加元素性能因參數有所區別,但都需注意數組容量不足時ArrayList會進行擴容產生性能消耗
    • add(E)在數組末尾添加元素,複雜度O(1)
    • add(int, E)在數組指定位置添加元素,複雜度O(n),n爲下標後的元素數目
    • addAll(Collection)在數組末尾添加集合元素,複雜度O(n),n爲集合中的元素數目
  • 刪除元素慢,remove()刪除元素,後面元素需逐個移動,複雜度O(n),n爲下標後的元素數目
  • 更改效率高,set(index, E)直接根據下標替換數組元素,複雜度O(1)
  • 查詢效率高,get(index)直接根據下標獲取數組元素,複雜度O(1)

綜上,如果與指定下標元素增刪操作更多的時候選擇ArrayList會導致數組需要進行多次的元素移動,性能消耗十分大,該情況更適合使用LinkedList,因LinkedList增刪元素時只需更改該元素上一個與下一個節點的指向即可,相當於從一個雙向鏈表中摘除一個元素。

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