ArrayList是java中的線性結構的一種表示方法,在java中使用頻率非常高,下面來一步一步分析其底層的實現。(JDK1.8)
一、構造函數
ArrayList的構造函數有三個,分別如下,
我們最常使用的的無參的構造函數,那么無參的構造函數是如何定義的那,
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
從上面看到無參構造函數,實際上是把elementData賦予了默認容量的一個數組DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
那么有參數的構造函數那,
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); } }
從上面可以看出首先判斷給定的初始容量是否大於0,如果大於0則使用此值生成Object數組。如果等於0則和無參構造函數是一樣的。如果小於0則拋出異常。在實際開發中強烈建議使用帶參數的構造函數(為什么下面說)。
最后一個構造函數如下,
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; } }
從上面的代碼中可以看出,把集合類c調用toArray方法,返回其數組賦給elementData,然后對elementData進行判斷如果長度不為0,這判斷elementData的類型是否為Object,如果不是則使用Object進行替換,統一處理成Obejct的類型,因為在java中所有的類的父類都是Object。
二、成員變量
在ArrayList中主要的成員變量有elementData和size,elementData代表底層ArrayList的存儲結構,即ArrayList是用數組來保存數據的;size指的是ArrayList中元素的個數(和elementData的長度區分開)。其他的成員變量像默認的初始容量為10。ArrayList在使用無參構造函數的時候分配內存的時候采用了懶加載的模式,即使用new ArrayList的時候底層不會分配空間,在真正向ArrayList中放元素的時候才會真正初始化數組長度。
三、常用方法
1、set/add
set和add方法的不同之處在於set方法會返回被覆蓋的值,而add則不會
set(int index,E element)
在index處插入元素element,其具體實現如下
public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }
從上面的代碼中可以看出,首先需要判斷index是否合法,
private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
也就是說如果index大於ArrayList中元素的個數,則拋出異常,這里有個疑問ArrayList是使用數組存儲,默認長度是10,那么向ArrayList中添加一個元素后其數組的長度為10,ArrayList中的元素為1,為什么不允許index大於size插入那。
在判斷完index不大於size后,把要插入的元素放在數組中下標為index的位置,返回之前的元素。
add(E e)
向ArrayList中插入元素e返回boolean類型,表示是否插入成功。
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
可以看到首先要確保數組的容量,
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }
判斷elementData是否為空數組,如果是空數組返回初始容量和size+1兩個數中大的那個,這里應該返回初始容量10。
下個方法是,
private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
如果minCapacity大於數組的長度則進行擴容,
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); }
上面代碼的意思是計算原數組的大小並且加上其右移1位(原大小的1.5倍)記為newCapacity。把要擴容到的大小和newCapacity比較,取兩者中比較大的那個作為數組的容量。把原先的數組中的元素拷貝到新的數組中。
在回頭來看add方法中的elementData[size++] = e;由於已經對數組進行了擴容,則可以把e放在size+1的位置上,返回true。
下面看add(int index,E e)
在index處插入e元素,
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++; }
驗證index的合法性
private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
然后確定底層數組的容量,判斷是否需要擴容,如果不需要則把e插入到index的位置,這里不是使用的循環移動插入,而是進行了數組的分部分拷貝,把index前的元素和index后的元素進行拷貝,最后把e插入在index處,這種方式比循環移動插入要快(使用底層的拷貝技術)。
2、get方法
根據index取得ArrayList中的元素,
public E get(int index) { rangeCheck(index); return elementData(index); }
判斷給定的index的值比較和size的大小,如果超過了size則拋出異常,否則返回其index處的值。
3、contains(Object o)方法
判斷o是否在ArrayList中,由於ArrayList使用數組存儲元素,所以它是允許存重復值的,而contains方法返回的則是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; }
可以看到ArrayList中允許存null值,且可以存多個。
如果o不是null,則從頭開始循環直到第一個為o的元素出現,返回其下標否則返回-1,最后根據indexOf的值大於0的返回true,否則false。
4、remove(int index)/remove(Object o)
從ArrayList中刪除元素,可以按照索引和元素進行刪除。按照元素刪除僅會刪除給定元素第一個的位置,其余不會刪除。
remove(int index)
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; }
首先依然是判斷刪除位置是否合法。如果合法則取得要刪除索引位置的元素值(返回),因為要刪除那么必然會進行數組的移動,即把刪除位置后的數組向前移動,這里使用的是數組的拷貝。
remove(Object o)
按照元素進行刪除,刪除ArrayList中的第一個為o的元素,
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的情況單獨處理,我們看不為null的情況的刪除,使用size遍歷整個數組,在數組中找到等於給定元素的下標,調用fastMove方法按照下標刪除,只會把o第一次出現的位置上的元素刪除,即重復的不會刪除。
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 }
上面還是按照下標進行刪除。
未完待續。