Java 線性表 ArrayList

數組

  • 簡單:數組是一種最簡單的數據結構
  • 占據連續內存:數組空間連續,按照申請的順序存儲,但是必須制定數組大小
  • 數組空間效率低:數組中經常有空閑的區域沒有得到充分的應用
  • 操作麻煩:數組的增加和刪除操作很麻煩

ArrayList 應用場景

優點:尾插效率高,支持隨機訪問。
缺點:中間插入或者刪除效率低。

排序不要使用 ArrayList,經常增刪改變位置不要用 ArrayList,效率很低。
只存放,隨機訪問,可以使用 ArrayList。
ArrayList 順序刪除節點要用迭代器從尾部往前刪,如果從前往后刪的話,會頻繁觸發 System.arraycopy。
遍歷 ArrayList 盡量使用迭代器。

優化ArrayList

如果我們要在集合中添加一百萬個數據,它只能是通過每次擴容1.5倍,每次將原數組數據放入新的數組,很明顯非常消耗資源。

構造方法

在構造ArrayList時傳入一個整型,就會直接構造一個該長度的集合,避免一直擴容造成的資源浪費。

使用普通方法

還可以使用ensureCapacity(int minCapacity)方法。參數為擴容目標大小。
如果要加入大量的數據,這種方式可以比傳統方式快10倍以上,加入數據越多,越明顯。

線性表

為了應對數組的缺點,可以使用線性表。
按物理結構劃分:順序存儲結構(順序表)、鏈式存儲結構。

順序表(ArrayList)

a1 是 a2 的前驅,ai+1 是 ai 的后繼,a1 沒有前驅,an 沒有后繼。
n 為線性表的長度 ,若 n==0 時,線性表為空表。
它繼承于AbstractList,實現了List、RandomAccess、Cloneable、 Serializable等接口。

ArrayList不是線程安全的,只能用在單線程環境下,多線程環境下可以考慮用Collections.synchronizedList(List l)函數返回一個線程安全的ArrayList類,也可以使用concurrent并發包下的CopyOnWriteArrayList類。

ArrayList實現了RandomAccess接口,即提供了隨機訪問功能。RandomAccess是java中用來被List實現,為List提供快速訪問功能的。在ArrayList中,我們可以通過元素的序號快速獲取元素對象,這就是快速隨機訪問;實現了Cloneable接口,能被克隆;實現了Serializable接口,因此它支持序列化,能夠通過序列化傳輸。

封裝了數組:

public class ArrayList<E> extends AbstractList<E> implements List<E>, 
    RandomAccess, Cloneable, java.io.Serializable {
    // transient是短暫的意思。對于transient 修飾的成員變量,
    // 在類的實例對象的序列化處理過程中會被忽略。 
    // 因此,transient變量不會貫穿對象的序列化和反序列化,
    // 生命周期僅存于調用者的內存中而不會寫到磁盤里進行持久化。
    transient Object[] elementData;
    private int size;
}

ArrayList源碼詳解

ArrayList內部通過一個Object數組來存儲數據:

transient Object[] elementData;

ArrayList使用size變量來表示實際存儲的元素個數:

private int size;

ArrayList有以下三個構造方法:

// 根據initialCapacity來創建具有指定初始容量的ArrayList
public ArrayList(int initialCapacity)
// 創建一個默認的ArrayList
public ArrayList()
// 根據其他集合來創建ArrayList
public ArrayList(Collection<? extends E> c)

詳細看一下這三個構造方法:

public ArrayList(int initialCapacity) {
    // 創建指定初始容量的ArrayList
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    }
    // 初始化容量指定為0,則用EMPTY_ELEMENTDATA數組
    else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    }
    // 否則,拋出IllegalArgumentException異常
    else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                            initialCapacity);
    }
}

EMPTY_ELEMENTDATA定義如下(即長度為0的Object數組):

private static final Object[] EMPTY_ELEMENTDATA = {};
public ArrayList() {
    // 默認ArrayList的內部數組是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

DEFAULTCAPACITY_EMPTY_ELEMENTDATA聲明如下:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA是一樣的,都是定義為了長度為0的Object數組,那它們有什么區別呢?它們兩個的主要區別在于添加第一個元素時,若elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA ,則程序會將其擴充為容量為DEFAULT_CAPACITY的數組,DEFAULT_CAPACITY定義為10,即通過默認的構造方法創建的ArrayList的初始容量是10。我們后面會詳細介紹數組的擴容。

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // 通過反射獲取數組類型,判定c.toArray類型是否為Object[]類型
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 若c為空,則內部數組為EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

add方法

ArrayList有兩個重載的Add方法:

// 在數組elementData尾部添加一個元素
public boolean add(E e)
// 在數組elementData指定位置index處添加元素
public void add(int index, E element)

add(E e)方法

我們先來看add(E e)方法,源碼如下:

// 在數組elementData尾部添加一個元素
public boolean add(E e) {
    // 容量大小判斷
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

該方法首先要判斷elementData數組的容量是否能夠容納新的元素,若不能,則需要進行擴容操作,然后將元素e放置在數組的size位置。ensureCapacityInternal(int)方法源碼如下:

private void ensureCapacityInternal(int minCapacity) {
    // 若elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // minCapacity = max(10, minCapacity)
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 針對數組最小容量,決定是否擴容
    ensureExplicitCapacity(minCapacity);
}

前面講到的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,在這里就起到作用了,若elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,則會將數組的最小容量設置為10。然后通過ensureExplicitCapacity(int)方法來判斷是否要擴容:

private void ensureExplicitCapacity(int minCapacity) {
    // 增加修改次數
    modCount++;
 
    // overflow-conscious code
    // 增加元素后,ArrayList中要存儲的元素個數為minCapacity
    // 若此時minCapacity > elementData原始的容量,則要按照minCapacity進行擴容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

擴容的最終操作是通過grow(int)方法來實現的:

private void grow(int minCapacity) {
    // overflow-conscious code
    // 獲取elementData的原始容量
    int oldCapacity = elementData.length;
    // 計算新的容量
    // 若原數組長度為偶數,那么新數組長度就恰好是原數組長度的1.5倍
    // 若原數組長度為奇數,那么新數組長度就恰好是原數組長度的1.5倍 - 1
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 若按照1.5倍進行擴容后,capacity仍然比實際需要的小,則新容量更改為實際需要的大小,即minCapacity
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果新數組的長度比虛擬機能夠提供給數組的最大存儲空間大,則將新數組長度更改為最大正數:Integer.MAX_VALUE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    // 按照新的容量newCapacity創建一個新數組,然后再將原數組中的內容copy到新數組中
    elementData = Arrays.copyOf(elementData, newCapacity);
}

擴容函數整體比較好理解,需要注意的是,若新容量過大,則會通過hugeCapacity(int)方法來進行容量判斷:

private static int hugeCapacity(int minCapacity) {
    // minCapacity < 0則表明數組容量已經超過了虛擬機所能表示的最大容量,拋出OutOfMemoryError
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    // 否則,若minCapacity > MAX_ARRAY_SIZE,則數組容量為Integer.MAX_VALUE,否則為MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

add(int index, E element)方法

add(int index, E element)方法源碼如下:

public void add(int index, E element) {
    // 判斷下標index的合法性
    rangeCheckForAdd(index);
 
    // 數組容量判斷
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 數組拷貝,將index到末尾的元素拷貝到index + 1到末尾的位置,將index的位置留出來
    System.arraycopy(elementData, index, elementData, index + 1,
                        size - index);
    elementData[index] = element;
    size++;
}

該方法與add(E e)方法類似,只是元素的插入位置不同,該方法需要調用rangeCheckForAdd(int)方法來對index進行合法檢驗:

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

若index下標不合法,則拋出IndexOutOfBoundsException異常。

remove方法

remove方法在ArrayList中同樣有兩種實現方式:

// 根據index下標刪除元素
public E remove(int index)
// 根據元素刪除
public boolean remove(Object o)

remove(int index)方法

remove(int index)方法源碼如下:

public E remove(int index) {
    // 下標合法性檢驗
    rangeCheck(index);
 
    // 修改次數加1
    modCount++;
    // 獲取舊的元素值
    E oldValue = elementData(index);
 
    // 計算需要移動的元素個數
    int numMoved = size - index - 1;
    // 將元素向前移動
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    // 將最后的元素值設置為null
    elementData[--size] = null; // clear to let GC do its work
 
    return oldValue;
}

這里需要注意一點的就是rangeCheck(int)方法:

private void rangeCheck(int index) {
    // 若index下標超出size,則拋出IndexOutOfBoundsException異常
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

這里只判斷了index超出了size,而不需要判斷index為負數的情況,這是為什么呢?

因為該方法總是在訪問數組之前被調用,在訪問數組時,會對下標為負數進行判斷,如果index為負數,則會拋出ArrayIndexOutOfBoundsException異常,所以在這里就沒有必要判斷了,避免冗余。

remove(Object o)方法

remove(Object o)方法源碼如下:

public boolean remove(Object o) {
    // 若刪除的元素為null
    if (o == null) {
        for (int index = 0; index < size; index++)
            // 若數組元素為null,則調用fastRemove方法快速刪除
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } 
    // 若刪除的元素不為null
    else {
        for (int index = 0; index < size; index++)
            // 找到要刪除的元素,調用fastRemove方法快速刪除
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

ArrayList刪除元素時,是分為元素為null和不為null兩種方式來判斷的,這也說明ArrayList允許添加null元素;同時,如果這個元素在ArrayList中存在多個,則只會刪除最先出現的那個。

刪除元素,采用了fastRemove(int)方法來快速刪除:

private void fastRemove(int index) {
    // 修改次數加1
    modCount++;
    // 計算需要移動的元素數目
    int numMoved = size - index - 1;
    // 將index之后的元素向前移動一位
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    // 將數組最后一位置為null
    elementData[--size] = null; // clear to let GC do its work
}

其他相關方法介紹

trimToSize()

trimToSize()源碼如下:

public void trimToSize() {
    // 修改次數加1
    modCount++;
    // trim
    if (size < elementData.length) {
        elementData = (size == 0)
            ? EMPTY_ELEMENTDATA
            : Arrays.copyOf(elementData, size);
    }
}

該方法的主要工作就是將數組容量修改為size大小,若size為0,則將數組設置為EMPTY_ELEMENTDATA,否則,通過Arrays.copyOf方法來創建新的數組。

該方法的主要存在意義就是:如果capacity被分配過大,那么可以通過這個方法,將ArrayList實例的capacity的大小修改為數組存儲元素的個數,從而縮減ArrayList的存儲空間。

contains(Object o)

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}
 
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

toArray()

public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}

該方法有可能會拋出java.lang.ClassCastException異常,如果直接用向下轉型的方法,將整個ArrayList集合轉變為指定類型的Array數組,便會拋出該異常,而如果轉化為Array數組時不向下轉型,而是將每個元素向下轉型,則不會拋出該異常,顯然對數組中的元素一個個進行向下轉型,效率不高,且不太方便。

toArray(T[] a)

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

該方法可以直接將ArrayList轉換得到的Array進行整體向下轉型(轉型其實是在該方法的源碼中實現的),且從該方法的源碼中可以看出,參數a的大小不足時,內部會調用Arrays.copyOf方法,該方法內部創建一個新的數組返回,因此對該方法的常用形式如下:

public static Integer[] toArray(ArrayList<Integer> v) {
    Integer[] array = (Integer[])v.toArray(new Integer[0]);
    return array;
}

Arrays.copyOf()、System.arraycopy()

ArrayList的源碼中大量地調用了Arrays.copyof()和System.arraycopy()方法,我們下面深入詳解一下這兩個方法:

ArrayList中用的比較多的Arrays.copyOf()方法定義如下:

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

該方法調用了其重載方法:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                        Math.min(original.length, newLength));
    return copy;
}

該方法實際上是在其內部又創建了一個長度為newlength的數組,調用System.arraycopy()方法,將原來數組中的元素復制到了新的數組中,下面來看System.arraycopy()方法:

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

該方法被標記了native,調用了系統的C/C++代碼,在JDK中是看不到的,但在openJDK中可以看到其源碼:

static void pd_conjoint_oops_atomic(oop* from, oop* to, size_t count) {
  // Do better than this: inline memmove body  NEEDS CLEANUP
  if (from > to) {
    while (count-- > 0) {
      // Copy forwards
      *to++ = *from++;
    }
  } else {
    from += count - 1;
    to   += count - 1;
    while (count-- > 0) {
      // Copy backwards
      *to-- = *from--;
    }
  }
}

JVM源碼主要思想就是,創建一個新的數組,然后通過上述方法將原數組的數據移動到新數組中。從注釋中可以看到,這種實現方式要優于C語言的memmove()方法,因為memmove()方法還需要進行內存清理工作。

該方法可以保證同一個數組內元素的正確復制和移動,比一般的復制方法的實現效率要高很多,很適合用來批量處理數組。Java強烈推薦在復制大量數組元素時用該方法,以取得更高的效率。

fail-fast機制

在ArrayList的源碼中,我們經常會看到modCount++這樣的代碼,其實,modCount是用來實現fail-fast機制的,fail-fast機制是Java集合中的一種錯誤機制,當多個線程對同一個集合的內容進行操作時,就會發生fail-fast事件,它是一種錯誤檢測機制,只能被用來檢測錯誤,因為JDK并不一定保證fail-fast機制一定會發生。fail-fast機制會盡最大努力來拋出ConcurrentModificationException異常。

fail-fast機制產生的最初原因是在于程序在對Collection進行迭代時,某個線程對該Collection的結構進行了修改。這時迭代器會拋出ConcurrentModificationException異常,從而產生fail-fast事件。如果單線程違法了規則,也同樣會拋出此異常。

迭代器在調用next()、remove()等方法時都要調用checkForComodification()方法:

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

該方法主要是檢測modCount是否等于expectedModCount,若不等于,則拋出ConcurrentModificationException異常。

在創建迭代器時,會將modCount的值賦給expectedModCount,所以在迭代期間,expectedModCount不會改變,在ArrayList中,無論add、remove還是clear方法,只要改變了ArrayList的元素個數,都會導致modCount改變,從而可能導致fail-fast產生。

fail-fast解決方案

1、在遍歷過程中,所有涉及到改變modCount的地方全部加上synchronized或直接使用Collections.SynchronizedList。但不推薦該方案,因為增刪產生的同步鎖可能會阻塞遍歷操作。

2、使用CopyOnWriteArrayList來替換ArrayList,比較推薦該方案。

CopyOnWriteArrayList是 ArrayList的一個線程安全的變體,其中所有可變操作(add、remove等)都是通過對底層數組的一次復制來進行操作的,在以下情況很適用:

  • 在不能或不想進行同步遍歷,但是又需要從并發中消除沖突時;
  • 遍歷操作的數量大大超過了可變操作的數量,即讀多寫少時。
    CopyOnWriteArrayList在copy的數組上進行修改,這樣就不會影響原數組中的數據,修改完之后,改變原有數據的引用即可。

對CopyOnWriteArrayList采用了一種讀寫分離的思想,對CopyOnWriteArrayList進行讀取操作不需要加鎖。但它存在以下缺點:

  • 因為要復制一份底層數組,所以內存占用比較多;
  • CopyOnWriteArrayList只能保證數據的最終一致性,不能保證數據的實時一致性。

所以,編寫程序時,要進行權衡利弊來選擇合適的數據結構。

原文鏈接:Java集合之ArrayList詳解_DivineH的博客-CSDN博客
參考:Java 線性表——ArrayList & LinkedList - 少元 - 博客園 (cnblogs.com)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容