JDK原始碼剖析-集合篇-ArrayList
1.ArrayList基本原理以及優缺點
1.1ArrayList基本原理
一句話講,在JDK中,ArrayList底層基於一個Object[]陣列來維護資料。
1.2ArrayList優缺點
缺點:
-
容量受限時,需要進行陣列擴容,進行元素拷貝會影響效能
-
頻繁刪除和往中間插入元素時,產生元素挪動,也會進行元素拷貝,影響效能
-
不是執行緒安全的
優點:
-
隨機訪問某個元素,效能很好
建議:
維護順序插入的資料,遍歷或者索引訪問很方便,預估資料大小,避免頻繁擴容。不適合頻繁的刪除和中間插入等操作。
2.ArrayList原始碼解析
2.1如何看原始碼
看核心原始碼和精讀原始碼的方式不太一樣。看核心原始碼,也就是核心部分的原始碼,一般也就是10-30%。你應該都是從一些入口開始看起,但是如果你要精讀原始碼,除了在閱讀核心原始碼的基礎上,你還需要將原始碼拆解開來,研究每一個元件的各個作用,之後再從入口開始,一行一行都讀懂。應該先看核心原始碼再精讀原始碼
看原始碼無論是主動還是被動,都是先有問題,再有技術。為了解決什麼問題,從而衍生出瞭解決問題的技術。
閱讀原始碼的技巧,說白了就是一些思想,常見的有:
-
自頂而下
-
從整體到細節
-
在主幹到分支
-
連猜帶蒙
-
結合功能場景逐個分析
方法就更多了,主要有:
靜態
-
看註釋
-
加註釋
-
畫圖
動態
-
Debug
-
觀察日誌或增加日誌
閱讀原始碼,需要很多技術基礎,的確沒錯。但是也分是什麼樣的原始碼,如果只是簡單的原始碼,需要的基礎知識會很少,比如JDK的原始碼。但是如果比較難的原始碼,Zookeeper、HDFS、Kafka、Dubbo這些原始碼,就需要掌握Netty,NIO,網路,Java集合和併發等基礎才能看得更好。當然如果沒有這些基礎也不是不能看的,就是理解可能不會深,對原始碼最後可能只是初步瞭解。所以還是那句話,不要一概而論。
2.2ArrayList原始碼整體
當你看ArrayList的原始碼,可以先看看它有哪些核心成員變數,剛開始,你肯定不知道哪些是核心的變數,所以簡單過一下即可,根據名字,型別,大概看看即可,看不懂也沒關係。之後看下有哪些方法,根據名字大概猜猜是做什麼的。做這些主要讓你瞭解基本的脈絡,對原始碼有個大體映象,千萬不要在這裡細究原始碼。你主要有哪些方法和成員變數、內部類就可以了。
比如,你可以看到ArrayList原始碼中,有幾個靜態成員,從名字上看像是容量大小相關的,還有size和elementData等成員變數,有幾個建構函式,
有一些ensureCapacity,calculateCapacity等私有方法,感覺也是和容量大小有關的。因為Capacity就是容量,大小的意思。還有很多你應該常用的方法add(),set(),get()等等。
2.3從ArrayList建構函式開始
之前我們提到過,看核心原始碼一般是從入口開始,也就常說的自頂而下。
首先你要有個程式碼Demo。之後從它的入口開始看起,程式碼清單如下:
import java.util.List; public class ArrayListDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); } }
這個很簡單的程式碼,入口是main函式,第一行就是建立了一個ArrayList,裡面的元素都是String型別,使用預設無參的建構函式,你點選建構函式,進入原始碼來看看:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; private int size; public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
可以看到,有一個成員變數叫elementData的Object[]陣列,這個印證了我們之前提到過的ArrayList底層基本原理是陣列。而且你記不記得之前,在看ArrayList原始碼脈絡的時候是不是已經看到過了這個變數呢?
預設無參的建構函式讓elementData指向了和空陣列DEFAULTCAPACITY_EMPTY_ELEMENTDATA一樣的地址。
如果各位如果有印象的話,DEFAULTCAPACITY_EMPTY_ELEMENTDATA這個變數也是你之前看到過的靜態成員變數。
上面還列了一個成員變數size,你可以連蒙帶猜一下,它應該是描述陣列大小的變數。而且size是int型別,大家都知道int的預設值是0。
所以小結一下,你知道了new ArrayList<>()這個動作,底層是使用了Object[]陣列,但是這個陣列預設是空的,大小為0。
上面我們主要通過看脈絡,連蒙帶猜的思想,看了一個簡單的不能再簡單的原始碼。不知道你有沒有感覺到這兩個思想。有了初步的感受。接下來我還要引入一個非常關鍵的方法:畫圖。
上面ArrayList的無參建構函式的原始碼,基本上如下圖所示:
2.4ArrayList第一次呼叫add方法
首先你要修改下你的Demo,修改如下:
import java.util.ArrayList; import java.util.List; public class ArrayListDemo { public static void main(String[] args) { List<String> hostList = new ArrayList<>(); //預設大小是0 hostList.add("host1"); } }
上面程式碼,假設你通過add方法向hostList添加了1個host主機地址。ArrayList第一次呼叫add方法發生了什麼?
你可以點選進去,看到如下程式碼清單:
/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */ public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
看之前,我又要多說兩句,原始碼的註釋和方法名,有時候能幫助你瞭解這個方法大致的作用。這是很關鍵的一點。比如註釋的意思是add方法是追加一個具體的元素到list的末尾;方法名ensureCapacityInternal,是確保內部容量的意思。看過之後,你應該可以猜到這方法大致是確認容量相關的,可能是判斷容量能否新增元素用的。
還要注意的是,你看一個方法的時候,不要著急直接從第一個行就開始看,也是先根據註釋和方法名有一個大概的認識後,你需要看下整個方法的結構之後再開始。比如呼叫了哪些其他方法,有幾個for迴圈或if結構。就像這個add方法,他主要有2步,第一步是呼叫了一個方法,應該是確保內部容量是否夠新增元素,第二步是一個數組基本賦值操作並且size++一下。如下圖所示:
2.5ArrayList的陣列大小為0也能可以新增元素?
接著當你知道了整個方法的脈絡,你再來看下細節,先看第一步:ensureCapacityInternal()。
這個方法入參是size + 1,size之前說你應該看到過,就是一個成員變數int size。它的預設值是0,所以size+1後,這個方法的實參就是1,那麼形參minCapacity的值就是1。(實參就是傳入方法實際的值,形參就是方法括號中使用的引數名)。可以看到ensureCapacityInternal的程式碼清單如下:
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
接著你可以看到這裡又呼叫了calculateCapacity方法和ensureExplicitCapacity方法。我們先進入calculateCapacity看下,你可以記下它的實參是minCapacity=1,elementData還記得麼?是ArrayList核心的那個成員變數Object[]陣列。calculateCapacity程式碼如下:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; private static final int DEFAULT_CAPACITY = 10; private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }
這裡你可以看熟悉的兩個成員變數:elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
在ArrayList無參建構函式的時候就看到過。第一次呼叫add方法的時候,它們倆的引用地址和值都是應該一樣的,因為在建構函式中有過這麼一句話this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
所以這裡進入第一個if條件,然後使用DEFAULT_CAPACITY和minCapacity作比較,可以看到DEFAULT_CAPACITY這個變數的值是10,也就是說這裡Math.max操作後會返回10。
到這裡你可以在完善下之前畫的圖,就會變成如下所示:
也就是說calculateCapacity返回後,回到ensureCapacityInternal這個方法,會傳入ensureExplicitCapacity的實參是10,因為calculateCapacity(elementData, minCapacity)= 10。
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
接著你又會進入ensureExplicitCapacity方法,看到如下程式碼:
private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
此時形參minCapacity也就是剛才傳入的10,你可以從名字上看這個方法,ensureExplicitCapacity意思是確保精確的容量的意思。還是先看下方法的脈絡,第一行有一個modCount++,之後你可以看到一行程式碼註釋,overflow-consciouscode意思是說具有溢位意思的程式碼,之後有一個if條件,滿足會進入grow方法。
到這裡你可以連蒙帶猜下,這裡的grow方法,是不是就是說容量不夠,會進行增長,也就是擴容呢?
因為我們知道,無參建構函式建立的ArrayList大小預設是一個為0的Object[]陣列elementdata。而且elementdata.length肯定是0,難道也能新增元素麼?肯定是不可以的。
接著我們逐行看下,第一行的modCount好像不太知道是什麼,你可以點選下它,看到它是一個成員變數,而且有一大堆註釋,好像也沒看懂什麼意思。所以這裡要告訴大家另一個看原始碼的思想了:抓大放小。比如這裡看上去modeCount和新增操作沒啥關係,就先跳過!(其實這個modCount是併發操作ArrayList時,fail-fast機制的實現)
目前執行程式碼如下圖所示:
2.6ArrayList 第一次新增元素如何擴容
你接著往下看,很明顯,minCapacity=10,elementData這個空陣列length肯定是0。所以10-0>0,會滿足if條件,進入grow方法,它的實參是10。它的程式碼清單如下:
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); }
grow方法形參minCapacity的值是10,你進入這個grow方法後,從脈絡上來看,有兩個if分支,有兩個區域性變數oldCapacity和newCapacity,還有一步elementData通過Arrays.copyOf方法的賦值操作。
而且oldCapacity應該是0,因為我們知道elementData陣列是空的。接著你會看到一句:intnewCapacity =oldCapacity+(oldCapacity >> 1);
這句話是什麼意思呢?其實就是oldCapacity右位移1位。如果你還記得計算機基礎中的話,右移一位,有點類似於除以2,底層是二進位制的運算而已。這裡可以舉個例子:
比如oldCapacity=10,如果先左移1,就是乘以2,在右移 1就是除以2,如下所示:
左移和右移1位舉例: |
二進位制 1010 十進位制:10 原始數 oldCapacity 二進位制 10100 十進位制:20 左移一位 oldCapacity = oldCapacity << 1; 二進位制 1010 十進位制:10 右移一位 oldCapacity = oldCapacity >> 1; |
那麼int newCapacity = oldCapacity +(oldCapacity >> 1); 其實這句話的意思就是在原有大小基礎上增加一半大小。
但是由於oldCapacity=0,所以增加一半大小,newCapacity=0+0>>1=0+0=0。
而此時minCapacity=10。這樣就會進入第一個if條件,因為newCapacity - minCapacity < 0,即0-10<0,然後進行了一步賦值操作,newCapacity就會變成和minCapacity一樣,值是10。
這裡你可以總結一下,也就是說如果建立ArrayList時,不指定大小, ArrayList第一次新增元素,會指定預設的容量大小為10。
你可以繼續完善你的圖,grow方法邏輯如下:
2.7ArrayList計算完擴容容量大小後,又幹了什麼?
除了上面計算擴容容量大小的程式碼,是grow的核心邏輯之一。
第一次新增元素時,在grow中,最後還會執行一行程式碼:
elementData=Arrays.copyOf(elementData,newCapacity);
這個也是grow方法中另一個核心步驟,陣列的拷貝。
你可以點選Arrays.copyOf,看看它底層做了些什麼。程式碼如下:
public static <T> T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); } public static <T,U> T[] copyOf(U[] original, int newLength , Class<? extends T[]> newType) { T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength)); return copy; }
還是從脈絡上看下,這裡Arrays.copyOf它實參是elementData(Object[]空陣列)和newCapacity=10。可以看到它內部直接呼叫了一個過載方法。
在過載方法中,首先呼叫了一個三元表示式和一個System.arraycopy方法呼叫,接著執行了
System.arraycopy(original, 0, copy, 0,Math.min(original.length,newLength));
這句話,它看樣子像是操作了original和 copy的樣子。
之後你再來仔細分析下細節。
2.8ArrayList的缺點,原因原來是在這裡!
根據傳遞的引數elementData的class是Object[].class的型別,可以看出三元表示式會執行(T[]) new Object[newLength]。
接著就會執行System.arraycopy這個方法。你可以查閱下JDK的API,它的主要是作用是陣列的拷貝。
由於這個方法的API不是很好理解。這裡我給大家講下,它的方法簽名如下:
System.arraycopy(Objectsrc,int srcPos,Object dest,int destPos, int length)。
這裡,如果大家遇見難以理解的程式碼,除了畫圖,另外一個方法就是舉例子。比如:
舉個例子:
假設:有兩個陣列src和dest,它們都有5個元素,0,1,2,3,4。即:src[0,1,2,3,4] dest[0,1,2,3,4]。 問題:執行System.arraycopy(src, 3, dest, 1,2)後是什麼意思呢? 答案:意思就是從src源陣列3位置開始,移動2個元素到dest陣列位置1, 從dest陣列的1位置開始覆蓋。 結果dest就會變為0,3,4,3,4
好了,你知道了這個API基本的使用,再來看原始碼中的程式碼:
System.arraycopy(original,0,copy,0,Math.min(original.length,newLength));
首先original就是我們傳遞進來的elementData。它的length是0。newLength是傳遞進來的newCapacity,也就是10。copy 是我們剛建立的Object[]陣列,長度為10。Math.min取最小後是0。也就是變成:
System.arraycopy(original,0,copy, 0,0);
這個就很好理解了,就是從original位置0開始移動0個元素到copy陣列,從copy陣列0位置開始覆蓋。這就等於什麼都沒拷貝,直接返回了我們新建立的陣列T[] copy,這個陣列大小為10。
最終Arrays.copyOf獲得了一個長度為10的陣列,但裡面沒有任何元素。接著這句話就執行完了。elementData = Arrays.copyOf(elementData, newCapacity);結束grow方法的也就執行完了。
而grow方法執行完就意味著ensureCapacityInternal執行完了。這個方法已經確保了內部陣列的容量大小可以放入新元素。
我們回到開始,執行完第一步,內部容量確保後,接著第二步直接執行了elementData[size++]= e;通過陣列的賦值操作,就完成第一次元素的添加了!
到這裡可以發現,當新增的時候,如果大小不夠就會進入grow方法,進入grow方法就會進行一次1.5倍的擴容。而且程式碼規範一般都建議我們要指定ArrayList的大小,如果不指定,可能會造成頻繁的擴容和拷貝,造成效能低下。
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
好了,到這裡ArrayList最常用的add(E e)方法的原始碼原理就已經研究透徹了。
最終的原始碼原理圖如下: