Java集合框架源码解析之ArrayList
ArrayList 可能是很多人使用得最為頻繁的容器類了,ArrayList 實現了 List 接口,是一個有序容器,即存放元素的順序與添加順序相同,允許添加相同元素,包括 null ,底層通過數組來實現數據存儲,容器內存儲的元素個數不能超出數組空間。所以向容器中添加元素時如果發現數組空間不足,容器會自動對底層數組進行擴容操作
ArrayList 的類聲明
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable從其實現的幾個接口可以看出來,ArrayList 是支持快速訪問,可克隆,可序列化的
包含的成員變量
//序列化IDprivate static final long serialVersionUID = 8683452581122892189L;//集合默認的初始大小private static final int DEFAULT_CAPACITY = 10;//如果外部為集合設置的初始化大小為 0,則 elementData 指向空數組對象 EMPTY_ELEMENTDATAprivate static final Object[] EMPTY_ELEMENTDATA = {};//如果在初始化集合時使用的是無參數的構造函數,則 elementData 指向空數組對象 DEFAULTCAPACITY_EMPTY_ELEMENTDATAprivate static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//包含實際元素的數組transient Object[] elementData;//集合大小private int size;elementData 是一個 Object 類型的數組對象,即可用來存放任何對象類型,也是 ArrarList 中用來存放數據的容器。而 ArrayList 是一個泛型類,我們在初始化時就直接指定了數據類型,Java泛型只是編譯器為我們提供的語法糖,方便我們在向 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);}}//使用默認的初始化大小public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}//傳入一份初始數據,以此進行初始化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 的時候傳入集合的初始化大小,這通常來說都是更為高效率一些的,因為如果是讓集合在賦值的過程中自動擴容,則可能會需要進行多次擴容操作,而每次擴容都需要復制原有數據到新數組,這會導致運行效率降低
添加/修改 元素
在獲取指定索引處的元素時,都是直接通過坐標指向該元素 (E) elementData[index],而無需從頭開始遍歷集合,所以說 ArrayList 的遍歷效率較高
//通過索引直接訪問數組@SuppressWarnings("unchecked")E elementData(int index) {return (E) elementData[index];}//獲取索引 index 處的元素值public E get(int index) {rangeCheck(index);return elementData(index);}//將索引 index 出的元素值置為 element,并返回原始數值public E set(int index, E element) {rangeCheck(index);E oldValue = elementData(index);elementData[index] = element;return oldValue;}ArrayList 在存入數據時相對來說就不是那么理想了
如果是直接向集合尾端添加數據 add(E e),則先檢查是否需要擴容,需要的話則創建一個新的符合大小的數組,并將原數組中的元素移到新數組中,再向數組尾端添加待存入的元素
如果是向集合非尾端位置添加數據 add(int index, E element),一樣需要先檢查是否需要擴容,然后將數組中索引 index 后的所有元素向后推移一位,然后將 element 插入到空出的位置上
由此看出來,向集合添加元素由于可能會導致數組擴容,從而導致數組元素的大量移動,所以說 ArrayList 存入數據的效率并不高
以上說的是存入單個元素,此外還有存入整個集合的情況
//向集合添加數據//如果待添加的數據不為空則返回 true,否則返回 falsepublic boolean addAll(Collection<? extends E> c) {Object[] a = c.toArray();int numNew = a.length;//檢查是否需要擴容ensureCapacityInternal(size + numNew);//將數組 a 復制到 elementData 的尾端System.arraycopy(a, 0, elementData, size, numNew);size += numNew;return numNew != 0;}//從指定索引處添加數據//如果待添加的數據不為空則返回 true,否則返回 falsepublic boolean addAll(int index, Collection<? extends E> c) {rangeCheckForAdd(index);Object[] a = c.toArray();int numNew = a.length;//檢查是否需要擴容ensureCapacityInternal(size + numNew);//需要移動的數組元素數量int numMoved = size - index;//因為要添加的數據可能剛好是從數組最尾端開始添加,所以 numMoved 可能為 0//所以只在 numMoved > 0 的時候才需要對數組的元素值進行移動,以此空出位置給數組 aif (numMoved > 0)System.arraycopy(elementData, index, elementData, index + numNew, numMoved);//將數組 a 包含的數據添加到 elementData 中System.arraycopy(a, 0, elementData, index, numNew);size += numNew;return numNew != 0;}移除元素
再看下移除元素的方法
因為數組是一種內存地址連續的數據結構,所以移除某個元素同樣可能導致大量元素的移動
擴容機制
以上多處說到了數組的擴容,這里就來看下數組的擴容機制
實際進行擴容操作的是 grow(int grow(int minCapacity)) 方法,參數 minCapacity 用于指定要求的最小空間,在擴容前,會先判斷如果將當前容量提升到當前的 1.5 倍是否能達到 minCapacity 的要求 ,如果符合要求則直接將數據擴充到當前的 1.5 倍容量,否則則擴充到 minCapacity ,構建一個新的符合大小的數組后,就將原數組中的元素復制到新數組中
由此可想到,如果在初始化 ArrayList 前已知目標數據的數據量,則最好使用 ArrayList(int initialCapacity)來進行初始化,直接讓底層數組擴充到目標大小,避免之后賦值過程中多次擴容
遍歷集合的方法
//遍歷集合元素@Overridepublic 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;//如果 modCount 值被改動,則直接停止遍歷并拋出異常for (int i=0; modCount == expectedModCount && i < size; i++) {//將集合元素依次傳遞給 accept 方法action.accept(elementData[i]);}if (modCount != expectedModCount) {throw new ConcurrentModificationException();}}遍歷并過濾集合的方法
//按照給定規則對集合元素進行過濾,如果元素符合過濾規則 filter 則將之移除@Overridepublic boolean removeIf(Predicate<? super E> filter) {Objects.requireNonNull(filter);//要移除的元素個數int removeCount = 0;//用于標記集合是哪個索引位置需要被移除final BitSet removeSet = new BitSet(size);final int expectedModCount = modCount;final int size = this.size;for (int i=0; modCount == expectedModCount && i < size; i++) {@SuppressWarnings("unchecked")final E element = (E) elementData[i];//依次判斷集合元素是否符合過濾規則if (filter.test(element)) {//set 方法將導致索引位置 i 的元素變為 trueremoveSet.set(i);removeCount++;}}if (modCount != expectedModCount) {throw new ConcurrentModificationException();}//只有 removeCount > 0 才說明需要移除元素final boolean anyToRemove = removeCount > 0;if (anyToRemove) {//集合移除指定元素后的大小final int newSize = size - removeCount;for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {//略過被標記為 true 的位置,直接跳到不需要移除元素的數組索引位i = removeSet.nextClearBit(i);//有效數據逐漸從尾部向頭部聚集elementData[j] = elementData[i];}//移除尾部的無效數據,幫助GC回收for (int k=newSize; k < size; k++) {elementData[k] = null;}this.size = newSize;if (modCount != expectedModCount) {throw new ConcurrentModificationException();}modCount++;}return anyToRemove;}//將集合元素遍歷傳遞給 operator,并將原始數據替換為 operator 的返回值@Override@SuppressWarnings("unchecked")public void replaceAll(UnaryOperator<E> operator) {Objects.requireNonNull(operator);final int expectedModCount = modCount;final int size = this.size;for (int i=0; modCount == expectedModCount && i < size; i++) {//依次傳遞數組元素給 apply 方法,并將其返回值替換原始數據elementData[i] = operator.apply((E) elementData[i]);}//不允許在排序的過程中集合被其它方法修改了數據結構(例如:移除元素)if (modCount != expectedModCount) {throw new ConcurrentModificationException();}modCount++;}迭代器
在這里有個小細節,ArrayList 里多處使用到了 modCount 這個參數,每當集合的結構發生變化時,modCount 就會遞增,當在對集合進行迭代操作時,迭代器會檢查此參數值,如果檢查到此參數的值發生變化,就說明在迭代的過程中集合的結構發生了變化,此時迭代的元素可能就并不是最新的了,因此會直接拋出異常
//返回集合迭代器public Iterator<E> iterator() {return new Itr();}//一個優化版本的迭代器private class Itr implements Iterator<E> {//lastRet 指向的元素的下一個元素的索引int cursor;//最后一個返回的元素的索引//如果值為 -1,說明還未返回過元素或者改元素被移除了int lastRet = -1;//用于驗證集合的數據結構在迭代的過程中是否被修改了int expectedModCount = modCount;//是否還有元素未被遍歷public boolean hasNext() {return cursor != size;}//獲取下一個元素@SuppressWarnings("unchecked")public E next() {checkForComodification();int i = cursor;//如果索引值超出集合的可索引范圍則拋出異常if (i >= size)throw new NoSuchElementException();Object[] elementData = ArrayList.this.elementData;//如果索引值超出數組的可索引范圍則拋出異常if (i >= elementData.length)throw new ConcurrentModificationException();//游標遞增cursor = i + 1;return (E) elementData[lastRet = i];}//移除 lastRet 位置的元素public void remove() {if (lastRet < 0)throw new IllegalStateException();checkForComodification();try {ArrayList.this.remove(lastRet);//因為 lastRet 位置原始的元素被移除了,所以此時 lastRet 指向的元素是原先 lastRet+1 位置的元素cursor = lastRet;lastRet = -1;//因為是 Itr 主動對集合進行修改,所以此處需要主動更新 expectedModCount 值,避免之后拋出異常expectedModCount = modCount;} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}//遍歷集合從索引 cursor 開始之后剩下的元素@Override@SuppressWarnings("unchecked")public void forEachRemaining(Consumer<? super E> consumer) {Objects.requireNonNull(consumer);final int size = ArrayList.this.size;int i = cursor;if (i >= size) {return;}final Object[] elementData = ArrayList.this.elementData;if (i >= elementData.length) {throw new ConcurrentModificationException();}//遍歷調用 accept 方法while (i != size && modCount == expectedModCount) {consumer.accept((E) elementData[i++]);}cursor = i;lastRet = i - 1;checkForComodification();}//判斷迭代器在遍歷集合的過程中,集合是否被外部改動了(例如被其它迭代器移除了元素)//如果是的話則拋出異常final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}}效率測試
最后來測試下 ArrayList 擴容次數的高低對其運行效率的影響
對三個 ArrayList 存入相同數據量的數據,但分別為 ArrayList 指定不同的初始化大小
可以看出來,各種方式之間的運行效率差距還是很大的
關于 ArrayList 的內容就講到這里了,一方面是篇幅所限,一方面我是覺得很多知識點其實也不需要怎么講,直接看源碼的話認知會更為深刻一點,因此我也把對 ArrayList 的詳細源碼注釋開源到了 GitHub 上,歡迎關注
源碼地址:Java_Android_Learn
總結
以上是生活随笔為你收集整理的Java集合框架源码解析之ArrayList的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mac远程桌面Microsoft Rem
- 下一篇: Centos7 安装 nginx 服务器