前言:作為一個常用的List接口實現類,日常開發過程中使用率非常高,因此有必要對其原理進行分析。
注:本文jdk源碼版本為jdk1.8.0_172
1.ArrayList介紹
ArrayList底層數據結構是數組(數組是一組連續的內存空間),默認容量為10,它具有動態擴容的能力,線程不安全,元素可以為null。
筆者在一次使用ArrayList的時候引起了一次線上OOM,分析傳送門:記一次ArrayList產生的線上OOM問題
1 java.lang.Object 2 ↳ java.util.AbstractCollection<E> 3 ↳ java.util.AbstractList<E> 4 ↳ java.util.ArrayList<E> 5 6 public class ArrayList<E> extends AbstractList<E> 7 implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
2.主要源碼分析
add(e):
1 public boolean add(E e) { 2 // 確認容量 3 ensureCapacityInternal(size + 1); // Increments modCount!! 4 // 直接將元素添加在數組中 5 elementData[size++] = e; 6 return true; 7 } 8 9 private void ensureCapacityInternal(int minCapacity) { 10 // 進一步確認ArrayList的容量,看是否需要進行擴容 11 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); 12 } 13 14 private static int calculateCapacity(Object[] elementData, int minCapacity) { 15 // 如果elementData為空,則返回默認容量和minCapacity中的最大值 16 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 17 return Math.max(DEFAULT_CAPACITY, minCapacity); 18 } 19 // 否則直接返回minCapacity 20 return minCapacity; 21 } 22 23 private void ensureExplicitCapacity(int minCapacity) { 24 // 修改次數自增 25 modCount++; 26 27 // overflow-conscious code 28 // 判斷是否需要擴容 29 if (minCapacity - elementData.length > 0) 30 grow(minCapacity); 31 } 32 33 private void grow(int minCapacity) { 34 // overflow-conscious code 35 // 原容量 36 int oldCapacity = elementData.length; 37 // 擴容,相當於擴大為原來的1.5倍 38 int newCapacity = oldCapacity + (oldCapacity >> 1); 39 // 確認最終容量 40 if (newCapacity - minCapacity < 0) 41 newCapacity = minCapacity; 42 if (newCapacity - MAX_ARRAY_SIZE > 0) 43 newCapacity = hugeCapacity(minCapacity); 44 // minCapacity is usually close to size, so this is a win: 45 // 將舊數據拷貝到新數組中 46 elementData = Arrays.copyOf(elementData, newCapacity); 47 } 48 49
分析:
其實add方法整體邏輯還是比較簡單。主要注意擴容條件:只要插入數據size比原來大就會進行擴容。因此如果在循環中使用ArrayList時需要特別小心,避免頻繁擴容造成OOM異常。
add(int index, E element):
1 public void add(int index, E element) { 2 // 越界檢查 3 rangeCheckForAdd(index); 4 5 // 確認容量 6 ensureCapacityInternal(size + 1); // Increments modCount!! 7 // 將index及其之后的元素往后移動一位,將index位置空出來 8 System.arraycopy(elementData, index, elementData, index + 1, 9 size - index); 10 // 在index插入元素 11 elementData[index] = element; 12 // 元素個數自增 13 size++; 14 }
分析:
整體邏輯簡單:越界檢查->確認容量->元素后移->插入元素。
get函數:
1 public E get(int index) { 2 // 越界檢查 3 rangeCheck(index); 4 // 獲取對應位置上的數據 5 return elementData(index); 6 } 7 8 private void rangeCheck(int index) { 9 if (index >= size) 10 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); 11 } 12 13 E elementData(int index) { 14 return (E) elementData[index]; 15 }
分析:
get操作簡單,理解容易。
remove(index):
1 public E remove(int index) { 2 // 越界檢查 3 rangeCheck(index); 4 5 // 修改次數自增 6 modCount++; 7 // 獲取對應index上的元素 8 E oldValue = elementData(index); 9 10 // 判斷index是否在最后一個位置 11 int numMoved = size - index - 1; 12 // 如果不是,則需要將index之后的元素往前移動一位 13 if (numMoved > 0) 14 System.arraycopy(elementData, index+1, elementData, index, 15 numMoved); 16 // 將最后一個元素刪除,幫助GC 17 elementData[--size] = null; // clear to let GC do its work 18 19 return oldValue; 20 }
分析:
remove邏輯還是比較簡單,但是這里需要注意一點是ArrayList在remove的時候,並沒有進行縮容。
remove(o):
1 public boolean remove(Object o) { 2 // 如果被移除元素為null 3 if (o == null) { 4 // 循環遍歷 5 for (int index = 0; index < size; index++) 6 // 注意這里判斷null是用的“==” 7 if (elementData[index] == null) { 8 // 快速remove元素 9 fastRemove(index); 10 return true; 11 } 12 } else { 13 for (int index = 0; index < size; index++) 14 // 這里判斷相等是用的equals方法,注意和上面對比 15 if (o.equals(elementData[index])) { 16 fastRemove(index); 17 return true; 18 } 19 } 20 return false; 21 } 22 23 private void fastRemove(int index) { 24 // 注意這里並未做越界檢查,畢竟叫fastRemove 25 // 修改次數自增 26 modCount++; 27 // 判斷是否是最后一個元素,這里的操作和remove(index)是一樣的 28 int numMoved = size - index - 1; 29 if (numMoved > 0) 30 System.arraycopy(elementData, index+1, elementData, index, 31 numMoved); 32 elementData[--size] = null; // clear to let GC do its work 33 }
分析:
remove元素的時候分為null和非null,並且是快速remove,並未做越界檢查。
retainAll:求交集
1 public boolean retainAll(Collection<?> c) { 2 // 判空 3 Objects.requireNonNull(c); 4 // 批量remove complement為true表示保存包含在c集合的元素,這樣就求出交集了 5 return batchRemove(c, true); 6 } 7 8 private boolean batchRemove(Collection<?> c, boolean complement) { 9 final Object[] elementData = this.elementData; 10 // 讀寫指針 讀指針遍歷,寫指針只有在條件符合時才自增,這樣不需要額外的空間 11 int r = 0, w = 0; 12 boolean modified = false; 13 try { 14 // 遍歷 15 for (; r < size; r++) 16 // 如果c集合中包含遍歷元素,則把元素放入寫指針位置(以complement為准) 17 if (c.contains(elementData[r]) == complement) 18 elementData[w++] = elementData[r]; 19 } finally { 20 // Preserve behavioral compatibility with AbstractCollection, 21 // even if c.contains() throws. 22 // 正常情況下,r與size是相等的,這里是對異常的判斷 23 if (r != size) { 24 // 將未讀的元素拷貝到寫指針后面 25 System.arraycopy(elementData, r, 26 elementData, w, 27 size - r); 28 w += size - r; 29 } 30 // 將寫指針后的元素全部置空 31 if (w != size) { 32 // clear to let GC do its work 33 for (int i = w; i < size; i++) 34 elementData[i] = null; 35 modCount += size - w; 36 size = w; 37 modified = true; 38 } 39 } 40 return modified; 41 }
分析:
將集合與另一個集合求交集,整體邏輯比較簡單的。通過讀寫指針進行操作,不用額外空間。注意complement為true,則將包含在c中的元素寫入相應位置。這樣就求出了交集,這里還要注意finally中的操作,異常與置空操作。
removeAll:求差集,但是這里只保留當前集合不在C中的元素,不保留C中不在當前集合中的元素。
1 public boolean removeAll(Collection<?> c) { 2 // 判空 3 Objects.requireNonNull(c); 4 // 批量remove,注意這里complement為false,表示保存不在c中的元素,這樣就求出差集了 5 return batchRemove(c, false); 6 }
分析:
邏輯和retainAll剛好相反,complement為false,保存不包含在C中的元素,這樣就求出差集了,注意這里是單向差集。
3.總結
以上分析了ArrayList的主要源碼,下面對其進行總結:
#1.ArrayList的底層數據結構為數組(數組是一組連續的內存空間),默認容量為10,線程不安全,可以存儲null值。
#2.ArrayList擴容條件,只要增加容量大於現有容量就會進行擴容,擴容量為原來的1.5倍,但是ArrayList不會進行縮容。
#3.ArrayList中有求交集(retainAll)和求差集(removeAll),注意這里的差集是單向交集。
by Shawn Chen,2019.09.14日,下午。