關於ArrayList的學習
ArrayList屬於Java基礎知識,面試中會經常問到,所以作為一個Java從業者,它是你不得不掌握的一個知識點。😎
可能很多人也不知道自己學過多少遍ArrayList,以及看過多少相關的文章了,但是大部分人都是當時覺得自己會了,過不了多久又忘了,真的到了面試的時候,自己回答的支支吾吾,自己都不滿意😥
為什么會這樣?對於ArrayList這樣的知識點的學習,不要靠死記硬背,你要做的是真的理解它!😁
我這里建議,如果你真的想清楚的理解ArrayList的話,可以從它的構造函數開始,一步步的讀源碼,最起碼你要搞清楚add這個操作,記住,是源碼😄
一個問題看看你對ArrayList掌握多少
很多人已經學習過ArrayList了,讀過源碼的也不少,這里給出一個問題,大家可以看看,以便測試下自己對ArrayLIst是否真的掌握:
請問在ArrayList源碼中DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA是什么?它們有什么區別?
怎么樣?如果你能很輕松的回答上來,那么你掌握的不錯,不想再看本篇文章可以直接出門右拐(我也不知道到哪),如果你覺得不是很清楚,那就跟着我繼續往下,咱們再來把ArrayList中那些重點過一遍!😎
你覺得ArrayList的重點是啥?
在我看來,ArrayList的一個相當重要的點就是數組擴容技術,我們之前學習過數組,想一下數組是個什么玩意以及它有啥特點。
隨機訪問,連續內存分布等等,這些學過的都知道,這里說一個似乎很容易被忽略的點,那就是數組的刪除,想一下,數組怎么做刪除?😏
關於數組刪除的一些思考
關於數組的刪除,我之前也是有疑惑,后來也花時間思考了一番,算是比較通透了,這里就提一點,數組並沒有提供刪除元素的方法,我們都是怎么做刪除的?
比如我們要刪除中間的一個元素,怎么操作,首先我們可以把這個元素置為null,也就把這個元素刪除掉了,此時數組上就空出了一個位置,這樣行嗎?
當我們再次遍歷這個數組的時候是不是還是會遍歷到這個位置,那么就會報空指針異常,怎么辦?是的我們可以先判斷,但是這樣的做法不好,怎么辦呢?
那就是我們可以把這個元素后面的所有元素統一的向前復制,有的地方這里會說移動,我覺得不夠合理,為啥?
復制是把一個元素拷貝一份放到其他位置,原來位置元素還存在,而移動呢?區別就是移動了,原本的元素就不存在了,而數組這里是復制,把元素統一的各自向前復制,最終結果就是倒數第一和第二位置上的元素是相同的。
此時的刪除的本質實際上是要刪除的這個元素的后一個元素把要刪除的這個元素給覆蓋了,后面依次都是這樣的操作,可能有點繞,自己想一下。
所以就引出了數組的刪除操作是要進行數組元素的復制操作,也就導致數組刪除操作最壞的時間復雜度是0(n)。
為什么說這個?因為對理解數組擴容技術很有幫助!
數組擴容技術
上面我們談到了關於數組的刪除操作,我們只是分析了該如何去刪除,但是數組並未提供這樣的方法,如果我們要搞個數組,這個刪除操作還是要我們自己寫代碼去實現的。
不過好在已經有實現了,誰嘞,就是我們今天的主角ArrayList,其實ArrayList就可以看作是數組的一個升級版,ArrayList底層也是使用數組來實現,然后加上了很多操作數組的方法,比如我們上面分析的刪除操作,當然除此之外,還實現了一些其他的方法,然后這就形成了一個新的物種,這就是ArrayList。
本質上ArrayList就是一個普通的類,對數組進行的封裝,擴展其功能
對於數組,我們還了解一點那就是數組一旦確定就不能再被改變,而這個ArrayList卻可以實現自動擴容,有木有覺得很高級,其實也沒啥,因為數組本身特性決定,ArrayList所謂的自動擴容其實也是新創建一個數組而已,因為ArrayList底層就是使用的數組。
我們的重點需要關注的是這個自動擴容的過程,就是怎么創建一個新的數組,創建完成之后又是怎么做的,這才是我們關注的重點。
接下來我們看兩種數組擴容方式。
Arrays.copyof
不知道你使用過沒,我們直接看代碼:
public static void main(String[] args) {
int[] a1 = new int[]{1, 2};
for (int a : a1) {
System.out.println(a);
}
System.out.println("-------------拷貝------------");
int[] b1 = Arrays.copyOf(a1, 10);
for (int b : b1) {
System.out.println(b);
}
}
代碼不多,很簡單,看看輸出結果你就明白了
ok,是不是很簡單,知道這個簡單用法就ok了,接下來看另外一種
System.arraycopy()
這個方法我們看看是個啥:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
看見沒,native修飾的,一般是使用c/c++寫的,性能很高,我們看看這里面的這幾個參數都是啥意思:
src:要拷貝的數組
srcPos:要拷貝的數組的起始位置
dest:目標數組
destPos:目標數組的起始位置
length:你要拷貝多少個數據
怎么樣,知道這幾個參數什么意思了,那使用就簡單了,我這里就不顯示了。
ps:以后復制數組別再傻傻的遍歷了,用這個多香😄
以上兩個方法都是進行數組拷貝的,這個對理解數組擴容技術很重要,而且在ArrayList中也有應用,我們等會會詳細說。
下面咱們開始看看ArrayList的一些源碼,加深我們對ArrayList的理解!
源碼中的ArrayList
一般我們是怎么用ArrayList的呢?看下面這些代碼:
ArrayList arrayList = new ArrayList();
arrayList.add("hello");
arrayList.add(1);
ArrayList<String> stringArrayList = new ArrayList<>();
stringArrayList.add("hello");
簡單,都會吧,就是new一個出來,不過上面的代碼我還想說明一個問題,當你不指定具體類型的時候是可以存儲任意類型的數據的,指定的話就只能存儲特定類型,為啥不指定可以存儲任意類型?
這個問題不做解釋,等會看源碼你就明白了。
看看ArrayList的無參構造函數
一般我們看ArrayList的源碼,都是從它的無參構造函數開始看起的,也就是這個:
new ArrayList();
好啦,走進去看看這個new ArrayList();構造函數長啥樣吧。
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
咋一看,代碼不多,簡單,里面就是個賦值操作啊,有兩個新東西elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA,這是啥?🤪
不着急,我們點進去看看
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
這不就是Object數組嘛,好像還真是的,那transient啥意思?它啊,你就記住被它修飾序列化的時候會被忽略掉。
好了,除此之外,就是個數組,對Object類型的。
不好像有點區別啊,DEFAULTCAPACITY_EMPTY_ELEMENTDATA已經指定是個空數組了,而elementData只是聲明,在new一個ArrayList的時候進行了賦值,也就是這樣:
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
咋樣?明白了吧,之前不就說了嘛,ArrayList底層就是一個數組的,這里你看,new之后不就給你弄個空數組出來嘛,也就是說啊,你要使用ArrayList,一開始先new一下,然后給你搞個空數組出來。
啥?空數組?空數組怎么行呢?畢竟我們還需要用它存數據嘞,所以啊,重點來了,我們看它的add,也就是添加數據的操作。
看看ArrayList的add
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
就是這個啦,ArrayList不就是使用add來添加數據嘛,我們看看是怎么操作的,咋一看這段代碼,讓我們感到比較陌生的就是這個方法了
ensureCapacityInternal(size + 1);
這是啥玩意,翻譯一下😂
確保內部容量?什么鬼,這里還有個size,我們看看是啥?
private int size;
就是一個變量啊,我們再看看這段代碼
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
尤其是
elementData[size++] = e;
知道了嘛?我們之前不是已經創建了一個空數組,不就是elementData嘛,這好像是在往數組里面放數據啊,不過不對啊,不是空數組嘛?咋能放數據,這不是前面還有這一步嘛
ensureCapacityInternal(size + 1);
是不是有想法了,這一步應該就是把數組的容量給確定下來的,趕緊進去看看
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
就是這個了,這一步很重要:
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
也好理解吧,就是先判斷下現在這個ArrayList的底層數組elementData 是不是剛創建的的空數組,這里肯定是啊,然后開始執行
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
minCapacity是個啥(重要)
說這個之前,你先得搞清楚這個minCapacity 是啥,它現在其實就是底層數組將要添加的第幾個元素,看看上一步
ensureCapacityInternal(size + 1);
這里size+1了,所以現在minCapacity 相當於是1,也就是說將要向底層數組添加第一個元素,這一點的理解很重要,所以從minCapacity 的字面意思理解也就是“最小容量”,我現在將要添加第一個元素,那你至少給我保證底層數組有一個空位置,不然怎么放數據嘞。
重點來了,因為第一次添加,底層數組沒有一個位置,所以需要先確定下來一共有多少個位置,就是獻給數組一個默認的長度
於是這里給重新賦值了(只有第一次添加數據才會執行這步,這一步就是為了指定默認數組長度的,指定一次就ok了)
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
這怎么賦值的應該知道嘛,哪個大取哪個,那我們要看看DEFAULT_CAPACITY是多少了
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
ok,明白了,這就是ArrayList的底層數組elementData初始化容量啊,是10,記住了哦,那么現在minCapacity就是10了,我們再接着看下面的代碼,也即是:
ensureExplicitCapacity(minCapacity);
進去看看吧:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
也比較簡單,現在底層數組長度肯定還不到10啊,所以我們繼續看grow方法
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);
}
咋一看,判斷不少啊,干啥的都是,突然看到了Arrays.copyOf,知道這是啥吧,上面可是特意講過的,原來這是要進行數組拷貝啊,那這個elementData就是原來的數組,newCapacity就是新數組的容量
我們一步步來看代碼,首先是
int oldCapacity = elementData.length;
得到原來數組的容量,接着下一步:
int newCapacity = oldCapacity + (oldCapacity >> 1);
這是得到新容量的啊,不過后面的這個oldCapacity >> 1有點看不懂啊,其實這oldCapacity >> 1就相當於oldCapacity /2,這是移位運算,感興趣的自行搜索學習。
知道了,也就是擴容為原來的1.5倍,接下來這一步:
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
因為目前數組長度為0,所以這個新的容量也是0,而minCapacity 則是10,所以會執行方法體內的賦值操作,也就是現在的新容量成了10。
接着這句代碼就知道怎么回事了
elementData = Arrays.copyOf(elementData, newCapacity);
不知道你發現沒,這里饒了一大圈,就是為了創建一個默認長度為10的底層數組。
底層數組長度要看ensureCapacityInternal
ensureCapacityInternal這個方法就像個守衛,時刻監視着數組容量,然后過來一個數值,也就是說要向數組添加第幾個數據,那ensureCapacityInternal需要思考思考了,思考啥呢?當然是看底層數組有沒有這么大容量啊,比如你要添加第11個元素了,那底層數組長度最少也得是11啊,不然添加不了啊,看它是怎么把關的
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
記住了這段代碼
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
它的存在就是為了一開始創建默認長度為10的數組的,當添加了一個數據之后就不會再執行這個方法,所以重難點是這個方法:
ensureExplicitCapacity(minCapacity);
也就是真正的把關在這里,看它的實現:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
怎么樣,看明白了吧,比如你要添加第11個元素,可是我的底層數組長度只有10,不夠啊,然后執行grow方法,干嘛執行這個方法,它其實就是用來擴容的,不信你再看看它的實現,上面已經分析過了,這里就不說了。
假如你要添加第二個元素,這里底層數組長度為10,就不需要執行grow方法,因為根本不需要擴容啊,所以這一步實際啥也沒做(有個計數操作):
然后就直接在相應位置賦值了。
小結
所以這里很重要的一點就是理解這一步傳入的值的意義:
ensureCapacityInternal(size + 1);
簡單點就是要向底層數組中添加第幾個元素了,然后開始進行一系列的判斷,容量夠的話直接返回,直接賦值,不夠的話就執行grow方法開始擴容。
主要判斷就在這里:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
具體的擴容是這里
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);
}
這里需要注意這段代碼
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
這段代碼只有在第一次添加數據的時候才會執行,也是為創建默認長度為10的數組做准備的,因為這個時候原本數組長度為0,擴容后也是0,而minCapacity 為默認值10,所以會執行這段代碼。
但是一旦添加數據之后,底層數組默認就是10了,再加上之前的判斷,這里的newCapacity 一定會比minCapacity 大,這個點需要了解。
看看ArrayList的有參構造函數
我們上面着重分析了下ArrayList的無參構造函數,下面再來看看它的有參構造函數:
ArrayList arrayList1 = new ArrayList(100);
看看這個構造函數張啥樣?
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);
}
}
我去,這不就是直接創建嘛,然后還有這個:
else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
}
我們看看這個EMPTY_ELEMENTDATA
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
ok,現在你可以回答我們開篇提的那個問題了吧。
我們以上對ArrayList的源碼有了一定的認識之后,我們再來看看ArrayList的讀取,替換和刪除操作時怎樣的?
ArrayList的其他操作
經過上面的分析,我相信你對ArrayList的其他諸如讀取刪除等操作也沒啥問題,一起來看下。
讀取操作
看源碼
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
代碼很簡單,rangeCheck就是用來判斷數組是否越界的,然后直接返回下標對應的值。
刪除操作
看源碼
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
return oldValue;
}
代碼相對來說多一些,要理解這個,可以仔細看看我上面對“關於數組刪除的一些思考”的分析,這里是一樣的道理。
替換操作
看源碼
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
其實就是把原來的值覆蓋,沒啥問題吧😄
和vector很像
這個想必大家都知道,ArrayList和vector是很像的,前者是線程不安全,后者是線程安全,我們看一下vector一段源碼就明白了
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
沒錯,區別就是這么明顯!
總結
到這里,我們基本上把ArrayList的相關重點都過了一遍,對於ArrayList來說,重點就是分析它的無參構造函數的執行,經過分析,我們知道了它有個數組拷貝的操作,這塊是會影響到它的一些操作的時間復雜度的,關於這點,就留給大家取思考吧!
好了,今天就到這里,大家如果有什么問題,歡迎留言,一起交流學習!
感謝閱讀
大家好,我是ithuangqing,一路走來積累了不少的學習經驗和方法,而且收集了大量的精品學習資源,現在維護了一個公眾號【編碼之外】,寓意就是在編碼之外也要不停的學習,主要分享java技術相關的原創文章,現在主要在寫數據結構與算法,計算機基礎,線程和並發以及虛擬機這塊的原創,另外針對小白還在連載一套《小白的java自學課》,力求通俗易懂,由淺入深。同時我也是個工具控,經常分享一些高效率的黑科技工具及網站。
對了,公眾號還分享了很多我的學習心得,可以一起探討探討!
關注公眾號,后台回復“慶哥”,2019最新java自學資源立馬送上!更多原創精彩盡在【編碼之外】