1. 程式人生 > 其它 >ArrayList原始碼剖析

ArrayList原始碼剖析

ArrayList原始碼剖析

ArrayList是我們常用的線性表實現之一,其底層的資料結構是陣列。ArrayList實現了List, RandomAccess, Cloneable, 以及Serializable介面,說明ArrayList支援List的基本操作,能隨機訪問,以及支援深克隆和序列化。與LinkedList不同的是,ArrayList沒有實現Queue介面,說明Java集合框架沒有將ArrayList作為佇列以及棧的實現類,這也是很顯然的,因為棧和佇列的特點就是頻繁的增刪操作,使用連結串列的效能大多數情況下是極大優於陣列的。下面就從屬性,構造器以及幾個基本的方法去剖析ArrayList的原始碼。

1. 基本的屬性概括

ArrayList的基本屬性包括下面幾個:

 1 //預設的初始陣列容量(最大長度)
 2 private static final int DEFAULT_CAPACITY = 10;
 3 //當陣列容量為0的時候的預設陣列(為什麼要宣告為static)
 4 private static final Object[] EMPTY_ELEMENTDATA = {};
 5 //當陣列容量為預設容量時候的預設陣列,防止浪費記憶體空間,所以初始為空,當新增元素時根據預設容量和最少需要的容量哪個大來得到實際容量並生成新的陣列。
 6 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
7 //ArrayList實際的資料結構形式,一個Object陣列 8 transient Object[] elementData; 9 //ArrayList的大小 10 private int size;

2. 構造器

ArrayList提供了三種構造器。與LinkedList一樣,ArrayList也有無參構造器和引數為集合的構造器,同時ArrayList也提供了引數為整形的構造器,下面是這三個構造器的原始碼分析。

首先是帶有整形引數的構造器,引數表示初始容量。這段程式碼的邏輯比較簡單,判斷初始容量引數是否大於等於0,如果小於0則丟擲非法引數異常,如果等於0,則當前物件的陣列引用靜態的空陣列,如果大於0,則new一個容量為初始容量引數的Object陣列,完成初始化。

 1 public ArrayList(int initialCapacity) {
 2         if (initialCapacity > 0) {
 3             this.elementData = new Object[initialCapacity];
 4         } else if (initialCapacity == 0) {
 5             this.elementData = EMPTY_ELEMENTDATA;
 6         } else {
 7             throw new IllegalArgumentException("Illegal Capacity: "+
 8                                                initialCapacity);
 9         }
10     }

其次是無參的構造器。我們會驚訝的發現,無參構造器並不是直接new一個容量為預設容量的陣列,而是引用了靜態的預設容量空陣列,之所以這麼做我想是因為,由無參構造器產生的ArrayList物件不一定會有元素在其中,為了節省記憶體空間,先使用靜態的預設容量空陣列,當新增元素的時候再判斷是否進行對elementData進行擴容。

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

最後是引數為集合的構造器。泛型的extends保證了集合引數的泛型是當前ArrayList的泛型的類或者其子類。這段程式碼說明,集合引數通過toArray方法轉換為Object陣列,之後根據集合引數的大小,如果等於0,則elementData引用靜態的空陣列,否則再判斷elementData是否是Object[]型別(為什麼?),如果不是Object[]型別,則呼叫copyOf方法以Object[]物件的形式複製會elementData

 1 public ArrayList(Collection<? extends E> c) {
 2         elementData = c.toArray();
 3         if ((size = elementData.length) != 0) {
 4             // c.toArray might (incorrectly) not return Object[] (see 6260652)
 5             if (elementData.getClass() != Object[].class)
 6                 elementData = Arrays.copyOf(elementData, size, Object[].class);
 7         } else {
 8             // replace with empty array.
 9             this.elementData = EMPTY_ELEMENTDATA;
10         }
11     }

3. add(E)以及add(int, E)方法

在呼叫add(E e)方法新增元素的時候,首先檢查當新增元素之後的大小是否大於當前陣列的大小,如果大於則呼叫grow(int minCapaciy)方法進行陣列的擴容, 否則將新的元素儲存在size下標的位置,size自增一。

概括來說,ArrayList的擴容操作目的是確保陣列的容量要滿足至少能存下minCapacity個元素,為了達到這個目的,不是直接將新容量設定成minCapacity,而是先嚐試在原容量的基礎上計算新容量,計算公式為newCapacity = 1.5 * oldCapacity。如果這個新的容量無法滿足目的,才將新容量設定成minCapacity,為什麼不直接將新容量設定成minCapacity而是先根據舊容量計算新容量,這個問題我也不是很清楚,查閱了外網的資料和sun javadoc,比較被認可的說法是這個操作是一種安全的賭博(safe bet)。

 1 public boolean add(E e) {
 2         ensureCapacityInternal(size + 1);  // Increments modCount!!
 3         elementData[size++] = e;
 4         return true;
 5     }
 6 
 7 private void grow(int minCapacity) {
 8         // overflow-conscious code
 9         int oldCapacity = elementData.length;
10         int newCapacity = oldCapacity + (oldCapacity >> 1);
11         if (newCapacity - minCapacity < 0)
12             newCapacity = minCapacity;
13         if (newCapacity - MAX_ARRAY_SIZE > 0)
14             newCapacity = hugeCapacity(minCapacity);
15         // minCapacity is usually close to size, so this is a win:
16         elementData = Arrays.copyOf(elementData, newCapacity);
17     }

add(ine index, E element)方法是將元素插入到指定的下標Index處。首先先檢查index是否合法,之後跟add(E e)一樣,確保陣列容量足夠,不同的地方在於,如果是在特定下標index處新增元素,則原本的[index, size-1]下標的元素要全部向後移一個位置,即對應的移動到[index + 1, size' - 1],注意size'=size + 1。

1 public void add(int index, E element) {
2         rangeCheckForAdd(index);
3 
4         ensureCapacityInternal(size + 1);  // Increments modCount!!
5         System.arraycopy(elementData, index, elementData, index + 1,
6                          size - index);
7         elementData[index] = element;
8         size++;
9     }

4. remove(int)和remove(Object)方法

remove(int index)刪除特定下標的元素,並將被刪除的元素返回。方法首先檢查index是否合法,之後呼叫elementData(int index)返回特定下標的元素,而不是直接用elementData[index]返回元素。之後要把index之後的所有元素前移一個位置,同樣是呼叫arraycopy的進行陣列的複製。最後將陣列最後的元素的下一個位置置為空方便GC回收。

 1 public E remove(int index) {
 2         rangeCheck(index);
 3 
 4         modCount++;
 5     //之所以需要elementData(int index)方法來讀取特定下標元素,是因為讀操作和寫操作不同。ArrayList的底層是Object陣列,寫操作是絕對安全的(所有的類父類都是Object,所以Object可以引用任何類)。但是讀操作不同,讀取返回的型別是泛型E,無法確保Object到E的型別轉換是安全的,這時候編譯器就會發出警告。如果在每一個需要讀取元素的地方都是用element[index]的方式獲取元素,則編譯器會發出大量的警告,不方便我們除錯並且看著心煩,如果我們將element[index]封裝到一個方法裡,在方法裡進行強制型別轉換,並且用 @SuppressWarnings("unchecked")讓編譯器閉嘴,則會更美觀些哈哈。
 6         E oldValue = elementData(index);
 7 
 8         int numMoved = size - index - 1;
 9         if (numMoved > 0)
10             System.arraycopy(elementData, index+1, elementData, index,
11                              numMoved);
12         elementData[--size] = null; // clear to let GC do its work
13 
14         return oldValue;
15     }

remove(Object o)方法與根據下標刪除的方法不同,其刪除之後不會將被刪除的元素返回。這個方法從前向後遍歷陣列,刪除遇到的第一個等於引數o的元素,這裡的等於形式的說法是這樣的,(o==null)?get(i)==null:o.equals(get(i))。

 1 public boolean remove(Object o) {
 2         if (o == null) {
 3             for (int index = 0; index < size; index++)
 4                 if (elementData[index] == null) {
 5                     //fastRemove與普通remove不同之處在於其不進行邊界檢查,因為迴圈是從0到size-1,肯定不會刪到邊界之外的元素。
 6                     fastRemove(index);
 7                     return true;
 8                 }
 9         } else {
10             for (int index = 0; index < size; index++)
11                 if (o.equals(elementData[index])) {
12                     fastRemove(index);
13                     return true;
14                 }
15         }
16         return false;
17     }

5. set(int, E)方法

set(int index, E element)方法將下標為index的元素修改為element,並返回舊的元素。

1 public E set(int index, E element) {
2     //下標檢查, 如果index >= size,則報IndexOutOfBoundsException。為什麼不檢查index是否為負數呢?因為如果index為負數,則之後的陣列訪問操作必然會丟擲ArrayIndexOutOfBoundsException,沒必要重複編碼來拋這個異常。
3     //IndexOutOfBoundsException和ArrayIndexOutOfBoundsException的關係和區別是,IndexOutOfBoundsException是發生於例如字串,陣列,以及其他集合在下標超出範圍的時候,是ArrayIndexOutOfBoundsException的父類,ArrayIndexOutOfBoundsException是其具體實現,只針對於訪問陣列的下標為負數或者大於等於陣列大小的情況。
4         rangeCheck(index);
5 
6         E oldValue = elementData(index);
7         elementData[index] = element;
8         return oldValue;
9     }