ArrayList源碼分析


前言:作為一個常用的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日,下午。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM