淺析Java源碼之ArrayList
面試題經常會問到LinkedList與ArrayList的區別,與其背網上的廢話,不如直接擼源碼!
文章源碼來源於JRE1.8,java.util.ArrayList
既然是淺析,就主要針對該數據結構的內部實現原理和部分主要方法做解釋,至於I/O以及高級特性就暫時略過。
變量/常量
首先來看定義的(靜態)變量:
class ArrayList2<E> //extends AbstractList<E> //implements RandomAccess, Cloneable, java.io.Serializable { privatestatic final long serialVersionUID = 8683452581122892189L; private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; private int size; }
這裏在一開始定義了6個變量,其中第一個跟序列化相關不用管,其余5個依次解釋一下:
DEFAULT_CAPACITY:代表容器ArrayList的初始化默認大小
Object[] EMPTY_ELEMENTDATA:一個空數組,在某些方法調用後(例如removeAll)會用到
Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA:默認空數組,未傳參初始化時默認為這個
Object[] elementData:保存著當前ArrayList的內容,該變量被標記為序列化忽略對象
int size:很明顯,當前ArrayList大小
需要註意的是,其中幾個被標記為static final變量。
構造函數
看完變量,接下來看構造函數部分,構造函數有3個重載版本,分別闡述如下。
1、無參版本
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
如果不傳任何參數直接初始化一個ArrayList,會得到上面默認的空數組。
2、int版本
public ArrayList(int initialCapacity) { // 正常情況會初始化一個指定大小的數組 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } // 傳0在實現上與不傳是一樣的 else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } // 亂傳就拋異常 else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
這基本上最普遍的情況,可以看出內部實現就是普通的數組。
3、Collection版本
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 { // 傳空集合相當於空數組 this.elementData = EMPTY_ELEMENTDATA; } }
如果初始化傳入一個集合,會將此集合作為ArrayList的初值。
這裏存在一個bug,即toArray方法返回的不一定是Object,雖然默認情況下是,但是如果被重寫就不一定了。
詳細問題可見另一位的博客:http://blog.csdn.net/gulu_gulu_jp/article/details/51457492
如果返回不是Object類型,會做向上轉型。
方法
接下來看看常用的方法。
首先是get/set方法:
public E get(int index) { rangeCheck(index); return elementData(index); }
public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }
可以看出十分簡單暴力,首先會進行範圍檢查,然後返回/設置對應index的元素。
簡單看一下rangeCheck:
private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
也十分簡單,主要是判斷所給的索引index是否大於數組的大小size,否則拋出異常。
獲取對應的值時,沒有直接用elementData[index],而是用了一個方法elementData(),看著有點混,看一下方法定義:
E elementData(int index) { return (E) elementData[index]; }
方法其實只是對取出來的值進行了類型轉換,保證了返回類型的準確。
接下來是add/remove方法
這兩個方法都有重載版本,但是並不復雜,而且都用的比較多。
首先看add的一個參數版本,會在尾部插入給定元素。
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
稍微講下源碼註釋的modCount,這個變量來源於java.util.AbstractList,專門用來計算容器被改動的次數,對於我這種菜鳥使用者來說沒啥用。
這裏會首先檢測下容器的容量,然後在尾部加入元素,並將size加1。
看看ensureCapacityInternal方法:
private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); }
原來這是一個皮包函數,當數組元素為空時,會進行參數修正,由於容器的默認大小為10,所以不會對10以下的容量進行檢測。
修正後,將10或者比10大的形參傳入ensureExplicitCapacity進行檢測:
private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity); }
這看起來也像個皮包方法,不過並不是,如果當前容器大小已經達到上限,會調用grow進行擴容:
private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
其實這裏的形參名字我覺得不是特別好,應該叫最小所需容量,即minNeededCapacity。
這裏首先會獲取當前容器大小,並進行擴容,這裏的擴容是這樣算的:
oldCapacity + (oldCapacity >> 1)
也就是如果之前為10,那麽新容量為10 + Math.floor(10/2) = 15。
得到新容量後,會與傳進來的 所需容量進行對比,如果還不夠,那就幹脆取所需容量為新容量。
第二個if是判斷擴容後的容量是否大於最大(數組可達)整數,看下MAX_ARRAY_SIZE變量定義就明白了:
/** * The maximum size of array to allocate. * Some VMs reserve some header words in an array. * Attempts to allocate larger arrays may result in * OutOfMemoryError: Requested array size exceeds VM limit */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
這裏的註釋有必要看一眼,簡單講就是有些JVM會在數組中加入一些東西,所以實際上數組大小是比理論上小一點的。這個很容易理解的,比如電腦硬盤,容量100G,可用容量其實會打個折扣,一個道理的。
為了完整,所以也看一下hugeCapacity函數的內部實現:
private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
參數檢測挺好玩,內部使用的函數還怕傳入負數。
這裏會將所需容量與最大可用安全容量作比較,如果實在沒辦法,就將容量設置為最大可用容量,至於這裏會不會出問題我也不知道。
回到grow方法,得到新的容量後,會調用Arrays.copyOf方法,這個方法是包內另一個類的方法,內部實現是調用System.arraycopy直接進行內存復制,效率很高,最後返回一個新數組,size為加大後的容量。
接下來看第二個重載的add方法:
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++; }
這裏的檢測不太一樣,多了一步,不過看一眼方法就明白了:
private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
由於這個重載方法是插入,所以需要進行數值檢測,如果插入索引大於數組大小或者小於0,拋個異常。
接下來是常規的容量檢測。
下一步的方法就是之前提到的System.arraycopy,該方法會將索引+1後面的元素全部復制到源數組,舉個簡單的例子:
如果原數組為[1,2,3,4],假設索引為1,經過這一步,數組會變為[1,2,2,3,4]。
最後是將對應索引的值賦為給定值,size++。
可以看出,在數組中間插入一個元素是非常耗時的,會變動索引後面的每一個數組元素。
接下來是remove,這個方法也有2個重載,一個是刪除給定索引,一個是刪除給定元素:
public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); 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 return oldValue; }
沒啥好講的,忽略檢測,一句話概括就是將對應索引-1的所有元素復制到原數組,然後size-1,並將末尾元素置null讓GC進行回收,最後返回刪除元素。
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; }
這個是第二個重載,對null進行了特殊處理,這個奇怪的東西只能用==來進行比較。
總體來講就是遍歷數組,如果找到了匹配元素,進行fastRemove,刪除成功返回true,否則返回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 }
跟第一個重載的remove很相似,只是移除了範圍檢測與返回值的處理,更快一些。
其余的方法大多數是上面的變種,沒什麽研究的必要了,有興趣的可以自行閱讀源碼。
淺析Java源碼之ArrayList