小豹子帶你看原始碼:ArrayList

LeopPro發表於2018-01-17

世界上最牛的 Java 程式碼去哪找?當然是 JDK 咯~計劃學習一下常見容器的原始碼。 我會把我覺得比較有意思或者玄學的地方更新到這裡。

以下 JDK 原始碼及 Javadoc 均從 java version "1.8.0_131" 版本實現中摘錄或翻譯 java.util.ArrayList


首先,開頭就很有意思,宣告瞭兩個空陣列:

116 行 - 126 行

/**
 * Shared empty array instance used for empty instances.
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * Shared empty array instance used for default sized empty instances. We
 * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
 * first element is added.
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
複製程式碼

有什麼差別?註釋上告訴我們,EMPTY_ELEMENTDATA 用於空陣列例項,而 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 用於預設的空陣列例項,他們的區別在於是否能獲知首次新增元素時對陣列的擴充量。這樣看我們也是一頭霧水,不如我們看一下他是怎樣應用的:

143行-166行

/**
 * Constructs an empty list with the specified initial capacity.
 *
 * @param  initialCapacity  the initial capacity of the list
 * @throws IllegalArgumentException if the specified initial capacity
 *         is negative
 */
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);
    }
}

/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
複製程式碼

我們看,當我們使用 new ArrayList() 建立 ArrayList 例項時,elementData 被賦值為 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而如果我們這樣建立例項 new ArrayList(0)elementData 將會被賦值為 EMPTY_ELEMENTDATA。這看似沒什麼區別,我們再看一下擴容陣列的函式。

222 行 - 228 行

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}
複製程式碼

這個函式中對 elementData 的引用進行判斷,如果引用是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 則會在 DEFAULT_CAPACITY(值為10)minCapacity 中選擇最大值為擴容後的長度。這裡相當於用 elementData 的引用值做一個標記:如果我們使用 new ArrayList() 建立例項,則 ArrayList 在首次新增元素時會將陣列擴容至 DEFAULT_CAPACITY 如果我們使用 new ArrayList(0) 建立例項則會按照 newCapacity = oldCapacity + (oldCapacity >> 1); (255行) 規則擴充例項。
那麼,為什麼這樣做。我們想,我們使用空構造器和 new ArrayList(0) 建立例項的應用場景是不一樣的,前者是我們無法預估列表中將會有多少元素,後者是我們預估元素個數會很少。因此ArrayList對此做了區別,使用不同的擴容演算法。
然而令我驚訝的是,通過引用判斷來區別使用者行為的這種方式,這是我想不到的,如果是我,我一定會再設定一個標誌變數。


238 行 - 244 行

/**
 * The maximum size of array to allocate.
 * Some VMs reserve some header words in an array.
 * Attempts to allocate larger arrays may result in
 * OutOfMemoryError: Requested array size exceeds VM limit
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
複製程式碼

這個變數顯然是陣列的最大長度,但是為什麼要 Integer.MAX_VALUE - 8 呢,註釋給了我們答案:一些 VM 會在陣列頭部儲存頭資料,試圖嘗試建立一個比 Integer.MAX_VALUE - 8 大的陣列可能會產生 OOM 異常。


246 行 - 262 行

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    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);
}
複製程式碼

此處是擴容陣列的核心程式碼,其計算新陣列的長度採用 oldCapacity + (oldCapacity >> 1),新陣列大概是原陣列長度的 1.5 倍,使用位運算計算速度會比較快。
然而我想說的並不是這個。而是這句話 if (newCapacity - minCapacity < 0)。在 ArrayList 類程式碼中,隨處可見類似 a-b<0 的判斷,那麼為什麼不用 a<b 做判斷呢?
下面是我寫的測試程式碼:

int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
int newCap = 65421;
System.out.println(newCap > MAX_ARRAY_SIZE);
System.out.println(newCap - MAX_ARRAY_SIZE > 0);
複製程式碼

這段程式碼的輸出是 false false,我們可以看到,兩種寫法在正常情況下是等效的。但是我們考慮一下如果 newCap 溢位呢?我們令 newCap = Integer.MAX_VALUE + 654321 輸出結果就變成 了false true。 這是為什麼?

newCap:                  -2147418228
                          10000000000000001111111110001100
MAX_ARRAY_SIZE:          2147483639
                          01111111111111111111111111110111
MAX_ARRAY_SIZE*-1:       -2147483639
                          10000000000000000000000000001001
newCap - MAX_ARRAY_SIZE: 65429
                          00000000000000001111111110010101
複製程式碼

我們看,newCap 由於溢位,進位覆蓋了符號位,因此 newCap 為負,我們將 newCap - MAX_ARRAY_SIZE 看成 newCap + MAX_ARRAY_SIZE*-1,加和時兩符號位相加又變成了 0(兩個大負數相加又一次溢位),結果為正,而這次溢位恰恰是我們想要的,得到了正確的結果。


649 行 - 675 行

/**
 * The number of times this list has been <i>structurally modified</i>.
 * Structural modifications are those that change the size of the
 * list, or otherwise perturb it in such a fashion that iterations in
 * progress may yield incorrect results.
 *
 * <p>This field is used by the iterator and list iterator implementation
 * returned by the {@code iterator} and {@code listIterator} methods.
 * If the value of this field changes unexpectedly, the iterator (or list
 * iterator) will throw a {@code ConcurrentModificationException} in
 * response to the {@code next}, {@code remove}, {@code previous},
 * {@code set} or {@code add} operations.  This provides
 * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
 * the face of concurrent modification during iteration.
 *
 * <p><b>Use of this field by subclasses is optional.</b> If a subclass
 * wishes to provide fail-fast iterators (and list iterators), then it
 * merely has to increment this field in its {@code add(int, E)} and
 * {@code remove(int)} methods (and any other methods that it overrides
 * that result in structural modifications to the list).  A single call to
 * {@code add(int, E)} or {@code remove(int)} must add no more than
 * one to this field, or the iterators (and list iterators) will throw
 * bogus {@code ConcurrentModificationExceptions}.  If an implementation
 * does not wish to provide fail-fast iterators, this field may be
 * ignored.
 */
protected transient int modCount = 0;
複製程式碼

這個變數很有意思,它是用來標記陣列結構的改變。每一次陣列發生結構改變(比如說增與刪)這個變數都會自增。當 List 進行遍歷的時候,遍歷的前後會檢查這個遍歷是否被改變,如果有改變則將丟擲異常。 這種設計體現了 fail-fast 思想,這是一種程式設計哲學。儘可能的丟擲異常而不是武斷的處理一個可能會造成問題的異常。這種思想在很多應用場景的開發(尤其是多執行緒高併發)都起著重要的指導作用。ErLang 就很提倡這種做法。

後記

看完 ArrayList 的原始碼,我心裡有這樣的感受:嚴謹,高效,還有很多我之前不知道的操作。自己的程式碼和大牛的程式碼差距還是很大的。看完這些我也不知道我能吸收多少……慢慢來吧。

相關文章