【資料結構】ArrayList原理及實現

AlbenXie發表於2018-08-08

一、ArrayList介紹

ArrayList是一種線性資料結構,它的底層是用陣列實現的,相當於動態陣列。與Java中的陣列相比,它的容量能動態增長。類似於C語言中的動態申請記憶體,動態增長記憶體。 
當建立一個陣列的時候,就必須確定它的大小,系統會在記憶體中開闢一塊連續的空間,用來儲存陣列,因此陣列容量固定且無法動態改變。ArrayList在保留陣列可以快速查詢的優勢的基礎上,彌補了陣列在建立後,要往陣列新增元素的弊端。實現的基本方法如下: 
1. 快速查詢:在實體記憶體上採用順序儲存結構,因此可根據索引快速的查詢元素。 
2. 容量動態增長: 當陣列容量不夠用時(表1),建立一個比原陣列容量大的新陣列(表2),將陣列中的元素“搬”到新陣列(表3),再將新的元素也放入新陣列(表4),最後將新陣列賦給原陣列即可。(從左到右依次為表1,表2、表3、表4) 
3. 這裡寫圖片描述

二、ArrayList繼承關係

ArrayList繼承於AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable這些介面。 
實現了所有List介面的操作,並ArrayList允許儲存null值。除了沒有進行同步,ArrayList基本等同於Vector。在Vector中幾乎對所有的方法都進行了同步,但ArrayList僅對writeObject和readObject進行了同步,其它比如add(Object)、remove(int)等都沒有同步。

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    }

ArrayList與Collection關係如下圖,實線代表繼承,虛線代表實現介面: 
這裡寫圖片描述

  1. AbstractList提供了List介面的預設實現(個別方法為抽象方法)。
  2. List介面定義了列表必須實現的方法。
  3. 實現了RandomAccess介面:提供了隨機訪問功能。RandmoAccess是java中用來被List實現,為List提供快速訪問功能的。在ArrayList中,我們即可以通過元素的序號快速獲取元素物件;這就是快速隨機訪問。
  4. 實現了Cloneable介面:可以呼叫Object.clone方法返回該物件的淺拷貝。
  5. 實現了 java.io.Serializable 介面:可以啟用其序列化功能,能通過序列化去傳輸。未實現此介面的類將無法使其任何狀態序列化或反序列化。序列化介面沒有方法或欄位,僅用於標識可序列化的語義。

三、ArrayList的實現

對於ArrayList而言,它實現List介面、底層使用陣列儲存所有元素。其操作基本上是對陣列的操作。下面進行具體的介紹:

1. 私有屬性

// 儲存ArrayList中資料的陣列
private transient Object[] elementData;
// ArrayList中實際資料的數量
private int size;

很容易理解,elementData儲存ArrayList內的元素,size表示它包含的元素的數量。 
有個關鍵字需要解釋:transient。 
Java的serialization提供了一種持久化物件例項的機制。當持久化物件時,可能有一個特殊的物件資料成員,我們不想用serialization機制來儲存它。為了在一個特定物件的一個域上關閉serialization,可以在這個域前加上關鍵字transient。

2.建構函式

ArrayList提供了三種方式的構造器,可以構造一個指定初始容量的空列表、構造一個預設初始容量為10的空列表以及構造一個包含指定collection的元素的列表,這些元素按照該collection的迭代器返回它們的順序排列的。

    // ArrayList帶容量大小的建構函式。
    public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
        // 新建一個陣列
        this.elementData = new Object[initialCapacity];
    }

    // ArrayList建構函式。預設容量是10。
    public ArrayList() {
        this(10);
    }

    // 建立一個包含collection的ArrayList
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        size = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }

3.元素儲存 
ArrayList是基於陣列實現的,當新增元素的時候,如果陣列大,則在將某個位置的值設定為指定元素即可,如果陣列容量不夠了,以add(E e)為例,可以看到add(E e)中先呼叫了ensureCapacity(size+1)方法,之後將元素的索引賦給elementData[size],而後size自增。例如初次新增時,size為0,add將elementData[0]賦值為e,然後size設定為1(類似執行以下兩條語句elementData[0]=e;size=1)。將元素的索引賦給elementData[size]不是會出現陣列越界的情況嗎?這裡關鍵就在ensureCapacity(size+1)中了。 
具體實現如下: 
(1) 當呼叫下面這兩個方法向陣列中新增元素時,預設是新增到陣列中最後一個元素的後面。記憶體結構變化如下: 
這裡寫圖片描述

// 新增元素e
    public boolean add(E e) {
        // 確定ArrayList的容量大小
        ensureCapacity(size + 1); // Increments modCount!!
        // 新增e到ArrayList中
        elementData[size++] = e;
        return true;
    }
// 將集合c追加到ArrayList中
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacity(size + numNew); // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

(2)當呼叫下面這兩個方法向陣列中新增元素或集合時,會先查詢索引位置,然後將元素新增到索引處,最後把新增前索引後面的元素追加到新元素的後面。 
這裡寫圖片描述

// 將e新增到ArrayList的指定位置
    public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        ensureCapacity(size + 1); // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        elementData[index] = element;
        size++;
    }
    // 從index位置開始,將集合c新增到ArrayList
    public boolean addAll(int index, Collection<? extends E> c) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacity(size + numNew); // Increments modCount
        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

(3)呼叫該方法會將index位置的元素用新元素替代 
這裡寫圖片描述

// 設定index位置的值為element
    public E set(int index, E element) {
        RangeCheck(index);
        E oldValue = (E) elementData[index];
        elementData[index] = element;
        return oldValue;
    }

4.元素讀取

// 返回此列表中指定位置上的元素。
    public E get(int index) {
        RangeCheck(index);
        return (E) elementData[index];
    }

5.元素刪除 
ArrayList提供了根據下標或者指定物件兩種方式的刪除功能。如下: 
romove(int index),首先是檢查範圍,修改modCount,保留將要被移除的元素,將移除位置之後的元素向前挪動一個位置,將list末尾元素置空(null),返回被移除的元素。 
這裡寫圖片描述

// 刪除ArrayList指定位置的元素
    public E remove(int index) {
        RangeCheck(index);
        modCount++;
        E oldValue = (E) elementData[index];
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index + 1, elementData, index, numMoved);
        elementData[--size] = null; // Let gc do its work
        return oldValue;
    }

6. 調整陣列容量ensureCapacity 
(1)從上面介紹的向ArrayList中儲存元素的程式碼中,我們看到,每當向陣列中新增元素時,都要去檢查新增後元素的個數是否會超出當前陣列的長度,如果超出,陣列將會進行擴容,以滿足新增資料的需求。陣列擴容通過一個公開的方法ensureCapacity(int minCapacity)來實現。在實際新增大量元素前,我也可以使用ensureCapacity來手動增加ArrayList例項的容量,以減少遞增式再分配的數量。

// 確定ArrarList的容量。
    // 若ArrayList的容量不足以容納當前的全部元素,設定 新的容量=“(原始容量x3)/2 + 1”
    public void ensureCapacity(int minCapacity) {
        // 將“修改統計數”+1
        modCount++;
        int oldCapacity = elementData.length;
        // 若當前容量不足以容納當前的元素個數,設定 新的容量=“(原始容量x3)/2 + 1”
        if (minCapacity > oldCapacity) {
            Object oldData[] = elementData;
            int newCapacity = (oldCapacity * 3) / 2 + 1;
            if (newCapacity < minCapacity)
                newCapacity = minCapacity;
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    }
  •  

從上述程式碼中可以看出,陣列進行擴容時,會將老陣列中的元素重新拷貝一份到新的陣列中,每次陣列容量的增長大約是其原容量的1.5倍。這種操作的代價是很高的,因此在實際使用時,我們應該儘量避免陣列容量的擴張。當我們可預知要儲存的元素的多少時,要在構造ArrayList例項時,就指定其容量,以避免陣列擴容的發生。或者根據實際需求,通過呼叫ensureCapacity方法來手動增加ArrayList例項的容量。 
(2) ArrayList還給我們提供了將底層陣列的容量調整為當前列表儲存的實際元素的大小的功能。它可以通過trimToSize方法來實現。程式碼如下:

// 將當前容量值設為 =實際元素個數
    public void trimToSize() {
        modCount++;
        int oldCapacity = elementData.length;
        if (size < oldCapacity) {
            elementData = Arrays.copyOf(elementData, size);
        }
    }

由於elementData的長度會被擴充,size標記的是其中包含的元素的個數。所以會出現size很小但elementData.length很大的情況,將出現空間的浪費。trimToSize將返回一個新的陣列給elementData,元素內容保持不變,length和size相同,節省空間。 
7.轉為靜態陣列toArray的兩種方法 
(1)呼叫Arrays.copyOf將返回一個陣列,陣列內容是size個elementData的元素,即拷貝elementData從0至size-1位置的元素到新陣列並返回。

// 返回ArrayList的Object陣列
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

(2)如果傳入陣列的長度小於size,返回一個新的陣列,大小為size,型別與傳入陣列相同。所傳入陣列長度與size相等,則將elementData複製到傳入陣列中並返回傳入的陣列。若傳入陣列長度大於size,除了複製elementData外,還將把返回陣列的第size個元素置為空。

// 返回ArrayList的模板陣列。所謂模板陣列,即可以將T設為任意的資料型別
    public <T> T[] toArray(T[] a) {
        // 若陣列a的大小 < ArrayList的元素個數;
        // 則新建一個T[]陣列,陣列大小是“ArrayList的元素個數”,並將“ArrayList”全部拷貝到新陣列中
        if (a.length < size)
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        // 若陣列a的大小 >= ArrayList的元素個數;
        // 則將ArrayList的全部元素都拷貝到陣列a中。
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

8.實現了Cloneable介面,進行資料淺拷貝

// 克隆函式
    public Object clone() {
        try {
            ArrayList<E> v = (ArrayList<E>) super.clone();
            // 將當前ArrayList的全部元素拷貝到v中
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError();
        }
    }

9.實現Serializable 介面,啟用其序列化功能

    // java.io.Serializable的寫入函式
    // 將ArrayList的“容量,所有的元素值”都寫入到輸出流中
    private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();
        // 寫入“陣列的容量”
        s.writeInt(elementData.length);
        // 寫入“陣列的每一個元素”
        for (int i = 0; i < size; i++)
            s.writeObject(elementData[i]);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

    // java.io.Serializable的讀取函式:根據寫入方式讀出
    // 先將ArrayList的“容量”讀出,然後將“所有的元素值”讀出
    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        // Read in size, and any hidden stuff
        s.defaultReadObject();
        // 從輸入流中讀取ArrayList的“容量”
        int arrayLength = s.readInt();
        Object[] a = elementData = new Object[arrayLength];
        // 從輸入流中將“所有的元素值”讀出
        for (int i = 0; i < size; i++)
            a[i] = s.readObject();
    }
  •  

相關文章