閱讀原始碼,從ArrayList開始
前言
為啥要閱讀原始碼?一句話,為了寫出更好的程式。
一方面,只有瞭解了程式碼的執行過程,我們才能更好的使用別人提供的工具和框架,寫出高效的程式。另一方面,一些經典的程式碼背後蘊藏的思想和技巧很值得學習,通過閱讀原始碼,有助於提升自己的能力。當然,功利的講,面試都喜歡問原始碼,閱讀原始碼也有助於提升通過面試的概率。
結合今天的主題,一個很簡單的問題,在剛學習集合時,我們都使用過如下程式碼,但是下面幾行程式碼有區別嗎?
List list1 = new ArrayList(); List list2 = new ArrayList(0); List list4 = new ArrayList(10);
有人可能會說,沒指定初始值就按預設值,指定了初始值就按指定的值構造一個數組。真的是這樣嗎?如果你對上面這個問題有疑惑,就說明你該看看原始碼了。
學習程式設計的過程千萬不要人云亦云,一定要親自看看。
如何閱讀原始碼,每個人的方式不同,這裡僅以自己習慣的方式來說。以今天的主題為例,ArrayList是幹嘛的?怎麼用?這就延伸到一條路線,先看類名及其繼承體系——它是幹嘛的,再看建構函式——如何造一個物件,當然,建構函式會用到一些變數,所以在此之前我們需要先了解下用到的常量值和變數值,最後,我們需要了解常用的方法以及它們是如何實現的。
對於閱讀大多數類基本都是按照:類名——>變數——>建構函式——>常用方法。
本文只會選取有代表性的一些內容,不會講到每一行程式碼。
類簽名
好像沒有類簽名這個說法,這裡是對照函式簽名來說的,簡單說就是一個類的類名以及它實現了哪些介面,繼承了哪些類,以及一些泛型要求。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
從上述程式碼可以看出,ArrayList實現了:
Cloneable, Serializable介面,具有克隆(注意深度拷貝和淺拷貝的區別)和序列化的能力,
RandomAccess介面,具有隨機訪問的能力,這裡說的隨機主要是基於陣列實現的根據陣列索引獲取值,後期結合LinkedList分析更容易理解。
List
AbstractList已經實現了List介面,AbstractList中已經實現了一些常見的通用操作,這樣在具體的實現類中通過繼承大大減少重複程式碼,需要的時候也可以重寫其中方法。
變數
//序列化版本號
private static final long serialVersionUID = 8683452581122892189L;
//常量,預設容量為10
private static final int DEFAULT_CAPACITY = 10;
//常量,初始化一個空的Object型別陣列
private static final Object[] EMPTY_ELEMENTDATA = {};
//常量,本質也是一個空的Object型別陣列,與EMPTY_ELEMENTDATA用於區別初始化時指定容量0還是預設不指定
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//變數,真正用來儲存元素的陣列名
transient Object[] elementData;
//陣列中實際儲存的元素數量,未初始化則預設為0
private int size;
上述變數中的大部分值都比較好理解,令人疑惑的事EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA,除了變數名,其他都一樣,好在註釋和後續的方法為我們說明了,簡單說,就是針對初始化時,不同的建構函式選用不同的變數名,即
List list1 = new ArrayList(); //此時用DEFAULTCAPACITY_EMPTY_ELEMENTDATA
List list2 = new ArrayList(0); //此時用EMPTY_ELEMENTDATA
為啥搞這麼麻煩,是大神們閒得慌嗎?顯然不是,不信?請繼續往下看。
構造方法
//不指定初始容量的建構函式
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//指定初始容量的建構函式
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);
}
}
//通過已有集合直接構造
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
如上所示,ArrayList有三個建構函式:
不指定容量的情況下,此時直接構造一個空的陣列,只有當新增第一個元素時,才會擴容為預設容量10。所以說並不是我們經常理解的直接構造一個容量為10的陣列,到此時我們才理解為啥很多時候一些規範建議我們指定初始容量,因為這樣可以減少一次擴容操作。注意,此時使用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA 。
指定容量時,小於0拋異常,大於0直接用指定的值構造一個數組,等於0時,也是構造一個空陣列,但是此時使用的是EMPTY_ELEMENTDATA。
有啥區別呢?關鍵在與擴容時的操作。繼續往下看。
記住,ArrayList的擴容操作只可能發生在新增元素時。
常用方法
ArrayList的常用方法非常多,這裡先排除一大批私有方法和內部類,看一下外部方法(尷尬,差一點一張圖截不下):
看起來很多,這裡只選取幾個常用的,其他的可以類比著看。
add(E e)
第一個最常用的方法,新增元素(add)
public boolean add(E e) {
//檢查陣列容量是否充足,不夠則擴容
ensureCapacityInternal(size + 1);
//注意,下方程式碼相當於elementData[size] = e; size++;
elementData[size++] = e;
return true;
}
可以看出,在新增元素時,第一步先檢查陣列容量是否充足,不夠的話進行擴容,add方法的關鍵在於檢查容量
檢查容量:ensureCapacityInternal(int minCapacity)
//檢查容量是否足夠,不夠則擴容
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//比較實際儲存元素+1與陣列的容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//若構造時不指定容量,則返回預設容量10或者現有實際元素+1中的最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//構造時指定了容量,不管是0還是大於0,都返回實際容量+1
return minCapacity;
}
//如果實際容量+1超過了現有容量(陣列裝不下了),則擴容
private void ensureExplicitCapacity(int minCapacity) {
//記錄修改次數,主要是為了遍歷元素時發生修改則快速失敗,此處不談。
modCount++;
// 如果現有元素+1大於陣列實際長度,則進行擴容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
關鍵來了,如何擴容
擴容方法:grow(int minCapacity)
private void grow(int minCapacity) {
// 舊容量為陣列長度
int oldCapacity = elementData.length;
//新容量為舊容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//新容量小於實際元素+1,則按實際元素+1擴容
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//新容量大於陣列最大長度,根據實際選擇容量為Integer.MAX_VALUE或者MAX_ARRAY_SIZE;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 將舊陣列元素複製到新陣列
elementData = Arrays.copyOf(elementData, newCapacity);
}
上述程式碼有一個關鍵方法Arrays.copyOf(elementData, newCapacity)用來複制集合中的元素,此處不再深入。
回到開始的問題
在建立ArrayList時,
不指定初始容量,即
List list1 = new ArrayList();
//此時,構造一個空的陣列,第一次新增元素時,將陣列擴容為10,並新增元素。
指定初始容量為0,即
List list2 = new ArrayList(0);
//此時,也構造一個空陣列,但變數名和上面不一樣。第一次新增元素時,將陣列擴容為1,並新增元素。
指定初始容量為10,即
List list4 = new ArrayList(10);
//直接構造一個容量為10的陣列,第一次新增元素時,不擴容。
所以說,如果我們大概確定將要使用的元素數量,應當在建構函式中指明,這樣可以減少擴容次數,一定程度上提升效率。
小結
到目前為止,只是簡單寫了下ArrayList的建構函式和add方法,大部分內容都還沒有深入。想要把每一個方法都寫到,其實很難,也沒必要。
通過上面的內容,回顧自己閱讀原始碼的過程,既要“不求甚解”,更要“觀其大略”,對於一些核心的過程,我們需要仔細分析;但是對沒有經驗的新手來說,弄清楚每個細節很難,有些內容現階段可能還沒法理解,把握整體結構很重要,先搞清楚大概,再對每一個細節深入。如果一開始就對某一細節一直深入,很可能迷失其中自己都走不出來了。
看到這裡,你問我是不是對ArrayList完全瞭解了,哈哈,顯然沒有。但是,寫到這裡的時候,我的理解又深刻了不少。
心裡覺得大概懂了不一定是真的理解,只有抱著把內容寫出來讓別人看明白的心態,才有可能加深理解。不知,你看明白了沒?