前言 前言 這篇文章的ArrayList源碼是基於jdk1.8版本的源碼,如果與前後版本的實現細節出現不一致的地方請自己多加註意。先上一個它的結構圖 ArrayList作為一個集合工具,對於我而言它值得我們註意的地方有: 那麼我就由這四個細節對ArrayList進行分析。 ArrayList的參數細 ...
-
前言
這篇文章的ArrayList源碼是基於jdk1.8版本的源碼,如果與前後版本的實現細節出現不一致的地方請自己多加註意。先上一個它的結構圖
ArrayList作為一個集合工具,對於我而言它值得我們註意的地方有:
- 參數的作用細節
- 擴容的細節
- 迭代的細節
- 特殊API的細節
那麼我就由這四個細節對ArrayList進行分析。
-
ArrayList的參數細節
ArrayList參數其實並不是特別多,值得我們拿出來講的那就更少了。下麵我通過一張圖的展示,同時列出一些值得我們談一談的參數:
- DEFAULT_CAPACITY、MAX_ARRAY_SIZE 兩個參數規定了ArrayList的預設長度和最大長度。但是,如果使用預設構造函數,在初始化ArrayList的時候,它的長度是0,只有第一次添加數據時,它才會擴容為10;
- EMPTY_ELEMENTDATA、DEFAULTCAPACITY_EMPTY_ELEMENTDATA 這兩個參數的值都是Object空數組,至於為什麼作者要寫兩個同樣的變數,那當然是為了區分不同方法當中的語義,使源碼更加易讀,當然這對於我們來說可以往後再關註。
- size ArrayList中的大小,註意這也可以指已存放的數據的個數,和下麵elementData的長度也還有一定的區別。
- elementData Object數組,這個是整個ArrayList最核心的參數之一,ArrayList存放的數據都在這裡,對ArrayList的增刪改查都基於這個Object數組。同時,它是被transient關鍵字修飾的,這意味著ArrayList需要進行序列化的時候,會把它忽略。那麼我們會有一個問題,elementData 裡面的數據難道不用序列化了嗎? 答案當然是需要的,但是它不是直接將一整個數組都序列化,而是通過方法writeObject(),把elementData 中有數據的位置序列化。通俗的話就是,它序列化elementData的前size個,而elementData的真實長度中,size後面的空間都認為是沒有數據的,如果也將它序列化會造成一定的流量浪費,影響傳輸性能。
- modCount 這個參數不是在ArrayList中聲明的,它是在父類AbstractList中聲明的,它的作用是記錄ArrayList的結構(增加或刪除)改變次數,以此來配合迭代器進行安全檢查,迭代器一旦發現modCount被修改了,則會拋出ConcurrentModificationException。
-
擴容的細節
首先,為什麼不講增刪改查直接談擴容呢?因為ArrayList的查找和修改的實現細節其實和普通的數組操作一樣,並沒有什麼特別的地方。而添加和刪除涉及到數組的動態調整,也就是我現在寫的擴容,其它的其實和普通的數組操作差不多。
那麼,當ArrayList執行一次add()方法的時候,它會有什麼樣的操作呢?首先我們先來看一個圖。這是一個方法嵌套,執行到最後,就能確保list的空間是安全的。
上面是執行add方法後的一系列調用流程。可以看出方法內在調用完ensureCapacityInternal()後,空間是能確保數據的填充的。而往下調用的方法中,我們只關註grow()方法就行,這是個擴容方法,具體的代碼和意圖如下所示。
1 /** 2 * Increases the capacity to ensure that it can hold at least the 3 * number of elements specified by the minimum capacity argument. 4 * 5 * @param minCapacity the desired minimum capacity 6 */ 7 private void grow(int minCapacity) { 8 int oldCapacity = elementData.length; 9 //新容量暫時為舊容量的1.5倍 10 int newCapacity = oldCapacity + (oldCapacity >> 1); 11 //這一步是確保擴容的時候,擴容的空間儘量合理,避免頻繁擴容 12 if (newCapacity - minCapacity < 0) 13 newCapacity = minCapacity; 14 //假設參數大於MAX_VALUE,設定最大容量為Integer.MAX_VALUE 15 if (newCapacity - MAX_ARRAY_SIZE > 0) 16 newCapacity = hugeCapacity(minCapacity); 17 //調用數組工具把數據覆蓋並開闢記憶體空間 18 elementData = Arrays.copyOf(elementData, newCapacity); 19 }
-
迭代的細節
ArrayList的迭代器採用的是fast-fail方式,也就是我們的快速失敗方式。這是什麼意思呢?我們直接貼出源碼的註釋來解釋。
在創建迭代器之後,除非通過迭代器自身的 remove 或 add 方法從結構上對列表進行修改,否則在任何時間以任何方式對列表進行修改,迭代器都會拋出 ConcurrentModificationException。因此,面對併發的修改,迭代器很快就會完全失敗,而不是冒著在將來某個不確定時間發生任意不確定行為的風險。
那麼 ,iterator通過什麼方式判斷列表被修改了呢?答案是在ArrayList內部還有一個內部 Iterator的實現類,裡面有一個參數expectedModCount,這個值與modCount 比較,若兩個值不相等,則拋出異常。
1 //源碼方法 2 final void checkForComodification() { 3 if (modCount != expectedModCount) 4 throw new ConcurrentModificationException(); 5 }
註意!我所說的迭代要註意的細節,指的是在迴圈過程中,伴隨著ArrayList結構上的修改,例如添加或刪除。如果只是簡單的迴圈遍歷輸出,其實各種方法沒有太大的區別。
下麵例舉集中常見的錯誤使用方式:
這種情況通常是迭代用迭代器,而對於修改則是用ArrayList的方法引起的,要解決這個問題只需要利用iterator的remove()方法即可。
1 ArrayList<Integer> list = new ArrayList<>(3); 2 list.add(111); 3 list.add(222); 4 list.add(333); 5 6 Iterator<Integer> iterator = list.iterator(); 7 while(iterator.hasNext()){ 8 Integer next = iterator.next(); 9 if (next.equals(111)){ 10 //錯誤的使用,在創建迭代器後使用list方法來remove,會拋出ConcurrentModificationException 11 list.remove(new Integer(111)); 12 } 13 System.out.println(next); 14 } 15 }
下麵這種情況如果你只是想要找到一個目標值,同時將這個值刪除並break退出是可以的。但是,如果你還要繼續遍歷下去,這種情況則會導致ArrayList集合遍歷的不完整。
1 ArrayList<Integer> list = new ArrayList<>(3); 2 list.add(111); 3 list.add(222); 4 list.add(333); 5 for (int i=0;i<list.size();i++){ 6 if (list.get(i).equals(222)){ 7 //執行這一步,size的值為2,導致333這個值沒有輸出就結束迴圈。 8 list.remove(i); 9 continue; 10 } 11 System.out.println(list.get(i)); //輸出結果:111 12 } 13 14 }
一般來說,當我們迭代有對ArrayList進行remove的需求的時候,可以利用迭代器來遍歷ArrayList的結構,這樣比較安全規範的且不會產生錯誤。
1 import java.util.ArrayList; 2 import java.util.Iterator; 3 4 public class ArrayListDemo { 5 public static void main(String[] args) { 6 ArrayList<Integer> list = new ArrayList<>(); 7 list.add(111); 8 list.add(222); 9 list.add(333); 10 Iterator<Integer> iterator = list.iterator(); 11 while (iterator.hasNext()){ 12 Integer next = iterator.next(); 13 if(next.equals(222)){ 14 iterator.remove(); 15 continue; 16 } 17 System.out.println(next); 18 } 19 } 20 }
-
特殊API的細節
- remove方法參數的不確定性。
就在我剛纔的示例代碼中就有一種隱藏的危險,當你的ArrayList存放的是Integer類型的時候,有一種場景下你需要移除一個值。你會理所當然的調用list.remove(222);這個方法來移除222這個值。但是,這個時候其實它是指移除索引位置在222上的值。這個時候就會有刪除錯誤或者範圍異常的發生。
1 Integer next = iterator.next(); 2 if (next.equals(222)){ 3 4 list.remove(222); 5 }
2. subList方法的實現及返回類型
下麵先列出subList方法返回在SubList內部類的繼承關係和構造方法。
1 private class SubList extends AbstractList<E> implements RandomAccess { 2 private final AbstractList<E> parent; 3 private final int parentOffset; 4 private final int offset; 5 int size; 6 7 SubList(AbstractList<E> parent, 8 int offset, int fromIndex, int toIndex) { 9 this.parent = parent; 10 this.parentOffset = fromIndex; 11 this.offset = offset + fromIndex; 12 this.size = toIndex - fromIndex; 13 this.modCount = ArrayList.this.modCount; 14 } 15 }
ArrayList中有一個subList方法可以用於對ArrayList的切割。一般我們要用List介面來接收返回的集合,但是其實它返回的具體類型是一個內部類SubList。與ArrayList肯定不是同一個類型,因此,如果你把它強制轉換為ArrayList則會發生錯誤。
1 public static void main(String[] args) { 2 ArrayList<Integer> list = new ArrayList<>(3); 3 list.add(111); 4 list.add(222); 5 list.add(333); 6 List<Integer> subList = list.subList(0, 1); 7 //編譯不報錯,運行時報錯 8 ArrayList worngList = (ArrayList)subList; 9 }
同時,從構造函數可以看出。SubList類中的集合其實是從ArrayList中直接賦值來的,它只是通過修改邊界範圍和size來構成一個新集合。也就是說,實質上SubList和ArrayList用的是同一個elementData數組。因此,當對SubList進行增刪改的時候,也會影響到ArrayList的存放的數據。示例代碼如下:
1 public static void main(String[] args) { 2 ArrayList<Integer> list = new ArrayList<>(3); 3 list.add(111); 4 list.add(222); 5 list.add(333); 6 List<Integer> subList = list.subList(0, 1); 7 subList.add(444); 8 subList.add(555); 9 }
我們通過debug可以看到,當添加到444,555的時候,兩個對象都出現了這兩個數據。