1. 程式人生 > >【深入理解java集合】-ArryList實現原理

【深入理解java集合】-ArryList實現原理

一、ArrayList簡介

1、概述

ArrayList是基於陣列實現的,是一個動態陣列,其容量能自動增長,類似於C語言中的動態申請記憶體,動態增長記憶體。

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

ArrayList實現了Serializable介面,因此它支援序列化,能夠通過序列化傳輸,實現了RandomAccess介面,支援快速隨機訪問,實際上就是通過下標序號進行快速訪問,實現了Cloneable介面,能被克隆。

每個ArrayList例項都有一個容量,該容量是指用來儲存列表元素的陣列的大小。它總是至少等於列表的大小。隨著向ArrayList中不斷新增元素,其容量也自動增長。自動增長會帶來資料向新陣列的重新拷貝,因此,如果可預知資料量的多少,可在構造ArrayList時指定其容量。在新增大量元素前,應用程式也可以使用ensureCapacity操作來增加ArrayList例項的容量,這可以減少遞增式再分配的數量。

注意,此實現不是同步的。如果多個執行緒同時訪問一個ArrayList例項,而其中至少一個執行緒從結構上修改了列表,那麼它必須保持外部同步。

2、關注要點

  • ArrayList使用的儲存的資料結構
  • ArrayList的初始化
  • ArrayList是如何動態增長
  • ArrayList如何實現元素的插入和移除

二、ArrayList的原始碼分析

對於ArrayList而言,它實現List介面、底層使用陣列儲存所有元素。其操作基本上是對陣列的操作。下面我們來分析ArrayList的原始碼:

1、屬性

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{    
//序列版本號    
private static final long serialVersionUID = 8683452581122892189L;     
//預設初始化容量    
private static final int DEFAULT_CAPACITY = 10;     
//空陣列,用來例項化不帶容量大小的建構函式    
private static final Object[] EMPTY_ELEMENTDATA = {};     
//儲存ArrayList中資料的陣列    
private transient Object[] elementData;     
//ArrayList中實際資料的數量    
private int size;

...

}

從原始碼中我們可以發現,ArrayList使用的儲存的資料結構是Object的物件陣列。其實這也不能想象,我們知道ArrayList是支援隨機存取的類似於陣列,所以自然不可能是連結串列結構。

有個關鍵字需要解釋:transient。 

Java的serialization提供了一種持久化物件例項的機制。當持久化物件時,可能有一個特殊的物件資料成員,我們不想用serialization機制來儲存它。為了在一個特定物件的一個域上關閉serialization,可以在這個域前加上關鍵字transient。

我們都知道ArrayList物件是可序列化的,但這裡為什麼要用transient關鍵字修飾它呢?檢視原始碼,我們發現ArrayList實現了自己的readObject和writeObject方法,所以這保證了ArrayList的可序列化。當寫入到輸出流時,先寫入“容量”,再依次寫出“每一個元素”;當讀出輸入流時,先讀取“容量”,再依次讀取“每一個元素”。

2、構造方法

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

//指定容量大小的建構函式
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() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

   //用Collection來初始化ArrayList
    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;
        }
    }

老版本java預設空參初始化時建立一個容量為10的陣列,Java1.8版本使用空陣列初始化elementData。EMPTY_ELEMENTDATA 實際上就是一個共享的空的Object陣列物件。初始化容量大小改為當第一次add的時候,這個陣列就會被初始化一個大小為10的陣列,個人認為與懶載入原理相同,節省資源消耗。

3、儲存元素

ArrayList提供了set(int index, E element)、add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)這些新增元素的方法。下面我們一一講解:

// 用指定的元素替代此列表中指定位置上的元素,並返回以前位於該位置上的元素。  
public E set(int index, E element) {  
   RangeCheck(index);  
 
   E oldValue = (E) elementData[index];  
   elementData[index] = element;  
   return oldValue;  
}    
// 將指定的元素新增到此列表的尾部。  
public boolean add(E e) {  
   ensureCapacity(size + 1);   
   elementData[size++] = e;  
   return true;  
}    
// 將指定的元素插入此列表中的指定位置。  
// 如果當前位置有元素,則向右移動當前位於該位置的元素以及所有後續元素(將其索引加1)。  
public void add(int index, E element) {  
   if (index > size || index < 0)  
       throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);  
   // 如果陣列長度不足,將進行擴容。  
   ensureCapacity(size+1);  // Increments modCount!!  
   // 將 elementData中從Index位置開始、長度為size-index的元素,  
   // 拷貝到從下標為index+1位置開始的新的elementData陣列中。  
   // 即將當前位於該位置的元素以及所有後續元素右移一個位置。  
   System.arraycopy(elementData, index, elementData, index + 1, size - index);  
   elementData[index] = element;  
   size++;  
}    
// 按照指定collection的迭代器所返回的元素順序,將該collection中的所有元素新增到此列表的尾部。  
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;  
}    
// 從指定的位置開始,將指定collection中的所有元素插入到此列表中。  
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;  
   }

當我們向一個ArrayList中直接新增一個物件或者Collection集合是,是採用末尾新增方式,只有通過下標向ArrayList中某個具體位置(末尾除外)新增資料元素時,才會通過System.arraycopy()方法,向後移動從index到length-1的所有元素,位移長度由資料個數決定。

當我們向一個ArrayList中新增資料的時候,首先會先檢查陣列中是不是有足夠的空間來儲存這個新新增的元素,minCapacity=ensureCapacity(size+1),或ensureCapacity(size+newNum),oldCapacity = elementData.length,即minCapacity與oldCapacity大小判斷。如果有的話,那就什麼都不用做,直接新增。如果空間不夠用了,那麼就根據原始的容量增加原始容量的一半, int newCapacity = oldCapacity + (oldCapacity >> 1),後面會具體講解。

4、元素讀取

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

由組數下標實現快速隨機讀取。

5、元素刪除:

5.1 romove(int index)

// 移除此列表中指定位置上的元素。  
 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;  
 }

首先是檢查範圍,修改modCount,保留將要被移除的元素,將移除位置index之後的元素向前挪動一個位置,將list末尾元素置空(null),返回被移除的元素。

5.2 remove(Object o)

public boolean remove(Object o) {  
    // 由於ArrayList中允許存放null,因此下面通過兩種情況來分別處理。  
    if (o == null) {  
        for (int index = 0; index < size; index++)  
            if (elementData[index] == null) {  
                // 類似remove(int index),移除列表中指定位置上的元素。  
                fastRemove(index);  
                return true;  
            }  
    } else {  
        for (int index = 0; index < size; index++)  
            if (o.equals(elementData[index])) {  
                fastRemove(index);  
                return true;  
            }  
        }  
        return false;  
    } 
}

首先通過程式碼可以看到,當移除成功後返回true,否則返回false。remove(Object o)中通過遍歷element尋找是否存在傳入物件,一旦找到就呼叫fastRemove移除物件。為什麼找到了元素就知道了index,不通過remove(index)來移除元素呢?因為fastRemove跳過了判斷邊界的處理,因為找到元素就相當於確定了index不會超過邊界,而且fastRemove並不返回被移除的元素。下面是fastRemove的程式碼,基本和remove(index)一致。

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; // Let gc do its work  
 }

5.3 removeRange(int fromIndex,int toIndex)

protected void removeRange(int fromIndex, int toIndex) {  
     modCount++;  
     int numMoved = size - toIndex;  
         System.arraycopy(elementData, toIndex, elementData, fromIndex,  
                          numMoved);  
   
     // Let gc do its work  
     int newSize = size - (toIndex-fromIndex);  
     while (size != newSize)  
         elementData[--size] = null;  
}
執行過程是將elementData從toIndex位置開始的元素向前移動到fromIndex,然後將toIndex位置之後的元素全部置空順便修改size。

執行過程是將elementData從toIndex位置開始的元素向前移動到fromIndex,然後將toIndex位置之後的元素全部置空順便修改size。

6、容量動態增長

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

public boolean add(E e) {
    ensureCapacityInternal(size + 1); // Increments modCount!! 
    elementData[size++] = e; 
    return true;
} 

1)ensureCapacityInternal的實現如下:

private void ensureCapacityInternal(int minCapacity) {
     if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
     }
     ensureExplicitCapacity(minCapacity); 
} 

DEFAULT_CAPACITY為:10

這也就實現了當我們不指定初始化大小的時候,新增第一個元素的時候,陣列會擴容為10.

2) ensureExplicitCapacity


private void ensureExplicitCapacity(int minCapacity) { 
    modCount++; // overflow-conscious code 
    if (minCapacity - elementData.length > 0) 
        grow(minCapacity); 
} 

這個函式判斷是否需要擴容,如果需要就呼叫grow方法擴容。

3)grow

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 = Arrays.copyOf(elementData, newCapacity); 
} 

我們可以看到grow方法將陣列擴容為原陣列的1.5倍,呼叫的是Arrays.copy方法。

在jdk6及之前的版本中,採用的還不是右移的方法

int newCapacity = (oldCapacity * 3)/2 + 1;

現在已經優化成右移了。

注意原始碼中通過if (newCapacity - MAX_ARRAY_SIZE > 0)控制集合的最大容量,MAX_ARRAY_SIZE= Integer.MAX_VALUE – 8。

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、轉為靜態陣列

ArrayList有兩個轉化為靜態陣列的toArray方法。

7.1 toArray()

第一個,呼叫Arrays.copyOf將返回一個數組,陣列內容是size個elementData的元素,即拷貝elementData從0至size-1位置的元素到新陣列並返回,返回的陣列length就等於size實際儲存的元素數量。

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

}

7.2 toArray(T[] a)

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

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

呼叫toArray()函式可能會丟擲"java.lang.ClassCastException"異常,但是呼叫toArray(T[] contents)能正常返回T[]。toArray()會丟擲異常是因為toArray()返回的是Object[]陣列,將Object[]轉換為其它型別(比如將Object[]轉換為Integer[])則會丟擲"java.lang.ClassCastException"異常,因為java不支援向下轉型。解決該問題的辦法是呼叫<T> T[] toArray(T[] contents),而不是Object[] toArray()。

8、ArrayList遍歷方式

ArrayList支援三種遍歷方式,下面我們逐個討論:

8.1 Iterator迭代器

Integer value = null;
Iterator it = list.iterator();
while (it.hasNext()) {
    value = (Integer)it.next();
}

8.2 隨機訪問

通過索引值去遍歷。由於ArrayList實現了RandomAccess介面,所以它支援通過索引值去隨機訪問元素。

Integer value = null;
int size = list.size();
for (int i = 0; i < size; i++) {    
value = (Integer)list.get(i); 
}

8.3 通過for迴圈遍歷

Integer value = null;for (Integer integ : list) { 
   value = integ;
}

通過測試,遍歷ArrayList時,使用隨機訪問(即通過索引號訪問)效率最高,而使用迭代器的效率最低。