List
是Java中非常常用的數據結構,而ArrayList
是其中最常用的實現,ArrayList
正如它的名稱一樣,它的本質是一個數組,所有的元素都會以數組的形式保存在內存中,然后提供各種操作數組的方法。
初始化
既然對象要存到數組,那么肯定就需要預先分配好內存,這也是ArrayList優化的核心因素。
ArrayList
提供了三個構造函數:
public ArrayList()
public ArrayList(int initialCapacity)
public ArrayList(Collection<? extends E> c)
先來說說最簡單的無參構造方法ArrayList()
:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是一個空的數組,采用這種初始化方式,就意味著我們需要使用ArrayList
的默認設置(默認不初始化數組),有意思的是在jdk1.6
中,這個方法里面只是簡單地調用了一下另外一個構造方法ArrayList(10)
。
再來看第二種構造形式:ArrayList(int initialCapacity)
:
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);
}
}
它則要求用戶輸入一個值初始化大小,ArrayList
根據這個值來分配數組的大小。
第三種構造方法:ArrayList(Collection<? extends E> c)
則是將Collection
對象轉換成數組(通過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;
}
}
在以上的源碼中所展示的一樣,ArrayList
擁有一個elementData
數組對象,它所有的儲存的元素都會保存在這個對象數組中。
添加元素
在初始化這個容器之后,我們現在最需要做的就是將數據存到ArrayList
里面,ArrayList
提供了幾種添加元素的方法:
public void add(int index, E element)
public boolean add(E e)
-
public boolean addAll(Collection<? extends E> c)
// 由AbstractCollection提供,內部其實使用了add()方法 -
public boolean addAll(int index, Collection<? extends E> c)
// 由AbstractList提供,內部也是使用了add()方法
那么讓我們由最簡單的add()
方法,來解析ArrayList
是如何將數據存入數組的。
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
這里使用一個參數size
,這個參數保存了當前ArrayList
中存儲元素數量。
ensureCapacityInternal(int size)
函數是一個私有方法,負責確認數組是否需要擴容,以及是否需要調整參數的大小,看看它內部具體的實現:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
這個函數中,我們發現有一句特意為無參構造方法寫的if
語句,DEFAULT_CAPACITY
的值為10,ensureExplicitCapacity
方法會判斷數組是不是真的需要擴容,然后會調用grow()
函數執行真正的數組擴容。
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 這是為迭代器快速失敗提供的一個參數,可以暫時忽略
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
所以,這意味著如果我們初始化的是一個空的ArrayList
,那么數組直到被add的時候才會擴容,相比1.6
中一經初始化就占用空間,是做了一些調整的。
那么grow()
方法中又是如何為數組擴容的呢:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 右移一位,二進制中高位減少一個數量級,即除以2
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);
}
從中可以看到ArrayList
在確認擴容之后利用Arrays.copyOf()
方法產生一個空間為原來1.5
倍的新數組,并將老的數組復制到新的數組中,也就是將老的元素,整體從一個位置挪到了另外一個位置,老的數組等待被GC釋放。
在確認數組的大小能夠容納下新的元素之后,我們回到add()
方法,新添加的元素添加到索引為size
值的位置,然后擴大size
的值。
說到擴容,ArrayList
為我們提供了一個公開的方法ensureCapacity()
,幫助我們手動擴容:
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
因為擴容總是1.5倍,有時候這并非我們希望的數組大小,如果我們事先已經預知代碼執行到某一段的時候,需要擴容的操作,那么我們就可以使用ensureCapacity()
方法手動擴容到期望的大小。
刪除元素
既然元素可以被添加的,那么也應該可以被刪除,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;
}
這個方法中,rangeCheck(index)
會檢查傳入的參數是否超出數組大小越界。
之后remove()
方法通過elementData()
方法查詢出要被刪除的數據:
E elementData(int index) {
return (E) elementData[index];
}
使用System.arraycopy
函數復制數據覆蓋原先的索引位置上的數據,并將最后一位制空并減少size
值,等待被GC回收內存。
查詢數據
ArrayList
可以通過get()
方法查詢數據:
public E get(int index) {
// 檢查傳入的參數是否越界
rangeCheck(index);
// 從 elementData 數組中取出對應索引位置的數據,強轉成對應的類型
return elementData(index);
}
由于是基于數組的容器,查詢的時候知道位置,數組大小可以被確定,查詢的復雜度為復雜度O(1),查詢的速度是非常快的。
當然既然是數組,更多的時候,我們需要遍歷整個數組,在Java8提供了lambda
之后,ArrayList
也提供了一個forEach()
方法完成整個數組的遍歷:
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
從中不難發現,forEach()
方法遍歷數組的本質其實還是利用了最基本的for
循環,然后直接通過索引位置取得數據。
總結
ArrayList
作為最常用的數據結構,需要更好地了解才能更好地使用它。從以上的分析中也不難看出ArrayList
最大的消耗來自于到處數組拷貝,幾乎所有操作元素的地方,都可能(對,只是可能)出現數組的拷貝,這本身是一個非常大的消耗,而大量老數組等待被GC也增加了GC的負擔。
所以使用ArrayList
最好的姿勢,應該是初始化的時候,估算好數組的大小,然后將元素放入容器之后,盡量不要再做一些修改容器的操作。