【集合框架】ArrayList源碼分析
一、前言
小胖覺得呀,Java集合框架,是面試考察的一個大重點!不管是你翻看大廠還是小公司,面試中都有對集合的要求,集合的知識也是非常多的。我在CSDN上找了很多的資料,包括框架圖、ArrayList考察點等。有很多比較好的資料,包括敖丙寫的3篇文章質量都是非常高的。本着分享技術的目的,也想自己好好的梳理一下。本系列文章包含:List、Set、Map、HashMap、CurrentHashMap、CopyOnWriteArrayList等。還是一個目的,讓你能搞懂一些集合的知識,以及集合的經典面試題。本文只有ArrayList的源碼分析
二、Java集合有哪些
上面兩個圖是我翻看了JDK1.8源碼之後,畫的一個簡單的圖,複雜的圖連線真的很多,完全看不了。網上的圖也都非常的複雜。
簡單解釋上面類的關係:
- Vector、ArrayList和LinkedList都實現了List接口
- HashSet實現了Set接口,LinkedHashSet繼承自HashSet
- SortedSet繼承了Set接口,TreeSet實現NavigableSet,NavigableSet繼承SortedSet
- Hashtable和hashMap實現Map接口,LinkedHashMap繼承HashMap
- SortedMap繼承了Map接口,TreeMap實現NavigableMap,NavigableMap繼承SortedMap
很複雜,真的很複雜!!!
三、面試考察重點在哪?
ArrayList底層原理、HashMap底層原理、CurrentHashMap與HashMap的區別。這三個內容是非常重要的,基本上都是必須要掌握的知識。
四、List家族有哪些?
1.Vector
Vector集合是一個比較古老的集合類,在Java1.1的時候就有了,Vector實現List集合,元素可以重複,順序按照插入的順序(即不會自動排序)。他是線程安全的,但是不推薦使用,因爲它使用了Synchronized同步機制的對象鎖的形式。
很多的方法都是下面的這種形式
public synchronized E set(int index, E element) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
2.ArrayList底層原理
ArrayList集合:實現List集合,元素可以重複,順序按照插入的順序(即不會自動排序)。他是線程不安全的,單線程考慮使用ArrayList。
然而答上面的內容很難滿足面試官
他們經常就會問你你知道ArrayList底層嗎?
我曾經第一次遇到這個問題的時候是懵的,底層不就是數組,順序表來實現…
其實他也就是想問你,是否看過源碼。
我覺得你可以從以下幾個方面來介紹:ArrayList的3個構造參數、add方法或插入方法、刪除方法、ArrayList擴容機制等。
ArrayList構造方法
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() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; //默認是創建一個空數組對象
}
public ArrayList(Collection<? extends E> c) { //把集合C的元素在創建的時候添加到數組當中
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
ArrayList的無參數構造方法,會傳入一個空的對象。一個int參數的構造方法,會傳入對象大小的對象。也可以傳入一個集合,當集合有值的時候,把對象複製到elementData中。
add方法、插入方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e; //在size中插入,然後size加1
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1, size - index); //元素後移,空出index位置
elementData[index] = element; //插入index
size++;
}
private void rangeCheckForAdd(int index) { //檢查index位置是否合法
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
ArrayList的add方法有兩個,直接add元素的會先檢查集合是否需要擴容,然後在末尾添加元素e,elementData[size++] = e;
第二個會檢查index的位置,防止越界。之後使用arraycopy的方法,把index之後的元素向後移以爲,再放入元素。
System.arraycopy(elementData, index, elementData, index + 1, size - index);
/**
@param src the source array. //原數組
@param srcPos starting position in the source array. //開始移動的位置
@param dest the destination array. //目標數組
@param destPos starting position in the destination data. //開始複製的地址
@param length the number of array elements to be copied. //複製的長度
*/
public static native void arraycopy(Object src, int srcPos,Object dest, int destPos,int length);
這個arraycopy是一個C語言寫的方法,它的5個參數:第一個是原來的數組,第二個是開始的位置,第三個是複製的目標數組,第四個是開始位置複製的地址,第五個是複製的長度。
把array數組中的元素全部複製到newArray中,應該怎麼寫?
System.arraycopy(array, 0, newArray, 0, size );
這樣是不是就理解了一些
ArrayList刪除方法
public E remove(int index) {
rangeCheck(index); //檢查索引的下標是否正確
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1; //需要移動的元素的個數
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved); //移動元素
elementData[--size] = null; // clear to let GC do its work //把移動完最後一個元素置爲null
return oldValue;
}
public boolean remove(Object o) { //按照對象,移除某一個對象
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) { //移除相應的對象
fastRemove(index);
return true;
}
}
return false;
}
ArrayList擴容機制
默認的初始化的情況下容量是10,add之後會有ensureCapacityInternal的判斷,把size+1和原來的elementData對象的Size進行對比,返回的DEFAULT_CAPACITY和minCapacity的最大值,當超過10之後,11比10大,進入到grow的函數,使用位運算,擴容原來的一半。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!! //這裏判斷是否擴容
elementData[size++] = e;
return true;
}
private static int calculateCapacity(Object[] elementData, int minCapacity) { //這裏的一層判斷真的很奇怪,如果數組中沒有元素,分配10個大小的元素,否則返回size+1
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) { //minCapacity 此時等於size+1
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) { //minCapacity 此時等於size+1
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0) //minCapacity比數組中元素的長度長的時候
grow(minCapacity); //做擴容的方法
}
private void grow(int minCapacity) { //擴容方法
// overflow-conscious code
int oldCapacity = elementData.length; //原來長度
int newCapacity = oldCapacity + (oldCapacity >> 1); //新的長度是原來的1.5倍,原來長度右移一位就是原來長度的0.5了
if (newCapacity - minCapacity < 0) //第一次插入的時候會使用,newCapacity=oldCapacity=0,所以直接等於10
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); //做擴容
}
看到這裏不經讓我想到一個題目:ArrayList(int initialCapacity)會不會初始化數組大小?也就是第二個構造函數
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);
}
}
初始化了一個空的數組,但是List的大小還是0,因爲List的大小是返回size的。
這道題是敖丙在這篇文章中提到的,我開始還有點懵,看完了擴容機制後基本瞭解了。
初始化的時候,會初始化對應空間的空數組,但是不會該表size的大小。當用戶第一次添加的時候ensureCapacityInternal(size+1);
進入到calculateCapacity方法由於數組是不爲空的,返回長度minCapacity=1,進入到grow方法後,oldCapacity等於6,newCapacity等於1,newCapacity還是等於6,elementData做擴容等於6。但是size還是1.
注意:size不等於ArrayList的容量,size是ArrayList中存在元素的個數。
上面的代碼,雖然在默認情況下是10的容量,但是還是無法set進去,因爲set會檢查index的索引的正確性,是通過size來確認的,size不是ArrayList的大小,而是ArrayList中含有的元素的個數。
3.LinkedList
LinkedList集合,實現List集合,元素可以重複,順序按照插入的順序,單鏈表。他是線程不安全的。
四、List集合經典面試題
ArrayList的底層原理知道嗎?
知道,我看過ArrayList的源碼。主要回答可以從增加、刪除元素、擴容機制、構造方法上面答。
ArrayList裏面可以放int?
ArrayList放int是可以正常使用的,但是這裏面涉及了自動裝箱。ArrayList裏面存放的內容是對象,對於基本的數據類型,我們只能使用對應的包裝類。
自動裝箱和自動拆箱你解釋一遍吧?
自動裝箱,在程序編譯後轉化成了對應的包裝類。自動拆箱,在程序編譯後轉化城裏對應的還原類。
大於128的比較就是false,自動裝箱和拆箱的知識還是有一點了,小胖就先不寫了。這一點就當課後作業吧。
好的!我就是懶
Vector和ArrayList他們如何擴容?
Vector增長原來的1倍,ArrayList默認的初始化容量是10,ArrayList增長原來的0.5倍
Vector每次擴容一次,增長一倍
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
說說 ArrayList,Vector, LinkedList 的存儲性能和特性?
ArrayList和Vector都是使用的數組的方式儲存數字,他們可以直接通過序號索引元素,但是在插入的時候,需要元素的移動等內存的操作,所以索引數據快但是插入慢。Vector和ArrayList的本質區別就是使用的Synchronized的代碼塊形式,他是線程安全的,但是性能是比ArrayList差的。LinkedList使用雙向鏈表,可以按需要索引,向前或向後遍歷,插入數據的速度較快。總結:ArrayList在查詢的時候速度較快,LinkedList在插入或者刪除的時候速度較快。
不做重複的事情吧,以後我還會繼續更新上面的內容,你們可以先看看大佬寫的內容。
阿里面試,面試官沒想到一個ArrayList,我都能跟他扯半小時
五、參考資料
ArrayList源碼; 博客https://blog.csdn.net/qq_35190492/article/details/103883964
未完待續
本人知識有限,如果有錯誤,歡迎各位指正。謝謝~