世界上最牛的 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 的原始碼,我心裡有這樣的感受:嚴謹,高效,還有很多我之前不知道的操作。自己的程式碼和大牛的程式碼差距還是很大的。看完這些我也不知道我能吸收多少……慢慢來吧。