ArrayList分析1-迴圈、擴容、版本
轉載請註明出處 https://www.cnblogs.com/funnyzpc/p/16407733.html
前段時間抽空看了下ArrayList
的原始碼,發現了一些有意思的東東,真的是大受裨益哈,尤其是版本問題?
所以,本篇部落格開始我將大概分三篇講講ArrayList
裡面一些有意思的點哈,由於原始碼大概一千八百逾行,裡面大多程式碼都很通俗,也有些部分存在重複的(Itr
以及SubList
的內部方法),因為大多通俗遂這裡不會逐行的分析哈,好了,現在開始~?
一.關於迴圈的一個問題
首先,我給出一個很easy的迴圈
:
public static void main(String[] args) {
for(int i = 0;i<8;i++){
System.out.print(i+"\t"); // 0 1 2 3 4 5 6 7
}
}
看起來很簡單吧,哈哈,這時我會問:各位有沒試過將i
提到for
迴圈外邊呢,像下面這樣:
public static void main(String[] args) {
int i;
for(i = 0;i<8;i++){
System.out.println(i);
}
System.out.println(i);// ?
}
上面第六行的i
會輸出什麼呢?真是個有意思的問題,這真是一個微小而有意思的問題,我們經常使用,卻很少利用for
去做一些別樣的事兒,ArrayList
就有一騷操作,
原本我是準備臆測出來,卻發現怎麼也理解不了,當然啦,這個問題接下來我會說到:探究這個問題我們先看看一個普通的for迴圈的結構
for(定義1;定義2;定義3){
//定義4 :迴圈內的語句塊
}
個人文采拙劣,這裡就用定義
一詞哈?
定義1: 這個地方我們經常會用int i=0;
這樣一個語句,其實這個地方是對迴圈的變數做一次定義
,這個地方的定義是一次性的,而且是第一次迴圈的時候會執行。
定義2: 這裡一般是個判斷性的表示式,而且這個地方的整體必須返回一個boolean
,這個很重要,既然這個地方只需要返回一個布林的結果,那麼你想過沒有,如果這個地方 我寫的是 (i<10 && i>=0)
會不會拋錯呢 ?
定義4: 這是迴圈內語句塊,通常我們會取到當前迴圈到的i
進行某些邏輯處理,這裡不是重點哈。
定義3: 這個地方是重點,一般我們會說每次迴圈後我們會將i--
或者i++
, 這種迴圈變數變化我們一般都會寫在這個位置,這是_very very normal_的,但問題是每次執行完定義4
的部分 就一定會執行定義3這個地方嘛? 答案是:一定會的!,為什麼呢,看看生成的位元組碼指令就知道了哈? :
0 iconst_0
1 istore_1
2 iload_1
3 bipush 8
5 if_icmpge 21 (+16)
8 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println : (I)V> //列印i
15 iinc 1 by 1
18 goto 2 (-16)
21 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
24 iload_1
25 invokevirtual #3 <java/io/PrintStream.println : (I)V> //列印i
28 return
以上是main函式內的完整位元組碼內容(jdk=java8), 可以看到指令內有兩處println
,自然第一個println
即是for迴圈
內的(標號12
處的),下面一行就很重要了,官方描述是:將區域性棧幀的索引+1(see: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.iinc),說明白些也就是將i加一,然後就到了標號18
這個位置,goto
是將當前語句指向標號2
這個位置 將store
的i
載入...到這裡也就很明白了, goto
指令是在i自增1
之後,可以完全確認迴圈外的println
列印的就一定是 8
!
看似簡單的操作 ArrayList 則時常使用,比如可以用i
迴圈,迴圈完成後,陣列的大小不就是這個i
了?以下ArrayList
->Itr
內的一段程式碼:
// 迴圈每個剩餘操作
// 這是java8提供給iterator的函式式迴圈介面,其使用方式如下
// ArrayList arr = new ArrayList();
// arr.add("a");
// arr.add("b");
// arr.add("c");
// System.out.println(arr);
// Iterator iterator = arr.iterator();
// iterator.next(); // a
// iterator.forEachRemaining(item-> System.out.println(item)); // b c
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
// 檢查是否為null,否則丟擲錯誤
Objects.requireNonNull(consumer);
// 獲取當前陣列大小並檢查迭代器的遊標位置是否大於陣列大小
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
// 老實說 elementData.length 與 ArrayList.this.size 是一對一關聯的,這裡這樣做似乎多餘
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
// 消費這個元素,同時將遊標位置+1
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic 在迭代結束時更新一次以減少堆寫入流量
// 因為i在以上已經+1了,所以這裡直接賦值以及重置當前迭代的索引位置(lastRet)
cursor = i;
lastRet = i - 1;
checkForComodification();
}
上面這是迭代器內的部分迭代方法的定義,可以看到有句 cursor = i;
這個地方的i
其實也就是size
哈,稍不注意就理解錯了,小問題讓大家見笑了...?
二.ArrayList
擴容
ArrayList
內部其實也就是維護了一個 Object
型別的陣列
,它具體是這樣定義的transient Object[] elementData;
,看起來是不是超簡單呢?呵呵呵,如果將ArrayList
看作成一個大水缸的話,這個elementData
就是水缸的本體,ArrayList
可以看做一個使用者態的介面,簡單理解它其實就是是水缸外層刷的油漆或蓋子抑或是漏斗。
打比方,如果這個水缸 (elementData )能裝四桶水,那麼這四桶水我們用 initialCapacity 這個變數來表示,當前實際上如果只有兩桶水,則這個水缸實際儲水容量(兩桶水)我們用size來表示,這樣就好理解了吧?
好了,如果預先知道將有8桶水
倒入缸內,那我們就要準備一個能容納8桶水
的水缸,對於程式碼就是這樣的:public ArrayList(int initialCapacity) {....}
當然,如果我們只允許用一個水缸
來儲存水的話,這個水缸當前如果是滿載
(8桶水),第9桶水
的時候就需要準備一個至少能容納9桶水的水缸,對於ArrayList
來說這時候就需要擴容了,程式碼是這樣的:
// 確保顯式容量(官方直譯,不懂直接看程式碼)
private void ensureExplicitCapacity(int minCapacity) {
// 這個變數記錄的是當前活動陣列被修改的次數
// 每新增一個(準確的說是一次)元素修改次數+1,如果是addAll也算做是+1
modCount++;
// overflow-conscious code 判斷是否溢位
// 可以簡單的理解 minCapacity 為當前需要保證的最小容量(具體大小為當前容量+1:這是對於當前add元素的個數而定的),elementData.length則為當前活動陣列的容量
// minCapacity 也為新增元素後所需陣列容量大小,如果(所需容量)大於當前(新增前)陣列容量即需要<b>擴容</b>
if (minCapacity - elementData.length > 0)
grow(minCapacity); //增長(擴容)
}
當然這時本著少騰挪多儲水的原則
,我們一般不會準備一個只能容納9桶水
的水缸,水缸太大也不好,容易浪費缸的容量維護也麻煩?,所以對於ArrayList
這個水缸,我們每次增長為現有容量的1.5
倍(多了50%左右,如果當前Capacity是10->增長到15,9->增長到13),具體對應到ArrayList的程式碼就是:
/**
* 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;
// 這裡的增長策略是 oldCapacity=10 -> newCapacity=15 oldCapacity=9 -> newCapacity=14
// 即 每一次增長的為上一次的一半
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 這裡個人覺得只是一個保險,對於類似addAll這樣的操作 newCapacity 可能小於一次add的數量
// 比如當前容量是10[oldCapacity:10->newCapacity:15],addAll(100)後所需的容量還是不夠 這時就會出現[newCapacity:100]
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 這裡也是一個保險,對於待擴容後的大小比陣列最大(MAX_ARRAY_SIZE)還要大的時候啟用hugeCapacity(minCapacity)
// 這裡呼叫 hugeCapacity 後頂多擴容8個大小 MAX_ARRAY_SIZE=2147483639(Integer.MAX_VALUE-8)
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 翻譯: minCapacity 通常接近 size,所以這是一個勝利
// Arrays.copyOf:
// 複製指定的陣列,截斷或填充空值(如有必要),使副本具有指定的長度。對於在原始陣列和副本中都有效的所有索引,
// 這兩個陣列將包含相同的值。對於在副本中有效但在原始副本中無效的任何索引,副本將包含 null。
// 當且僅當指定長度大於原始陣列的長度時,此類索引才會存在。結果陣列與原始陣列的類完全相同。
elementData = Arrays.copyOf(elementData, newCapacity);
}
上面有一句很重要 : int newCapacity = oldCapacity + (oldCapacity >> 1);
,每次換用更大的水缸時都會將之前缸內的水兌過來
,對應ArrayList
就是這句:elementData = Arrays.copyOf(elementData, newCapacity);
算是很形象吧?,這只是簡單的理解,擴容
也還有很多內容,比如什麼時候擴容,擴容一個單位不夠怎麼辦,滿了怎麼辦???等等問題....
三.ArrayList
中的版本管理
一開始大家會覺得這是個奇怪的問題,ArrayList中為啥會有版本,版本做什麼用?
首先,我詳細解答第一個問題:ArrayList
中為什麼有版本?,首先先看一段ArrayList
的原始碼,關於Iterator
的:
(以下為第一段程式碼)
public Iterator<E> iterator() {
// 雖然與都會返回一個迭代器,但是iterator只能單向迴圈,且不能實現增刪改查
// 詳見: https://www.cnblogs.com/tjudzj/p/4459443.html
return new Itr();
}
(以下為第二段程式碼)
public E next() {
// 版本檢查
checkForComodification();
// 這個將遊標賦予i,然後檢查是否i是否超出當前陣列索引位置(size)
// 我暫時沒看出以下三行跟 hasNext() 有多少區別。。。,而且checkForComodification內也是做了安全檢查了的
// 總結就是:十分沒必要啊...
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
// 不大明白為啥要再整個引用 ,通過這個新引用索引返回陣列值
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 因為迭代器每次迴圈前都會呼叫 hasNext ,故此推測這裡也應該將遊標+1
cursor = i + 1;
// 需要說明的是這個 lastRet 是個成員變數,而i只是個方法內臨時變數而已
// 所以每迴圈一次這個 lastRet 需要記錄為當前返值前的當前索引位置
return (E) elementData[lastRet = i];
}
(以下為第三段程式碼)
//// example:
// ArrayList arr = new ArrayList();
// arr.add("a");
// arr.add("b");
// arr.add("c");
// ListIterator listIterator = arr.listIterator();
// arr.remove("a");// throw error
// Object previous = listIterator.previous();
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
額,由於只是擷取了部分程式碼,我先簡單講講上面三段
程式碼,第一段
程式碼很明顯,我們使用迭代器的時候首先呼叫的就是集合類的 iterator()
方法,而 iterator()
內部 只做了一件事兒:new Itr()
,現在知道迭代器是一個類物件
,這點很重要。
繼續看第二段
:next()
方法,這個方法內部第一行程式碼 是 checkForComodification()
, 這是一個較為特殊的存在,點進去會看到以上第三段
程式碼,if判斷
內有兩個引數 ,一個是 modCount
(直譯為修改次數),第二個引數為 expectedModCount
(預期修改的次數),點到這個引數定義,它只有一句很簡單的定義int expectedModCount = modCount;
,是不是很迷糊??
在next()
內還有一句也很重要 Object[] elementData = ArrayList.this.elementData;
,這句估計很好懂了,Itr
迭代器內使用的陣列其實也就是ArrayList
中維護的陣列物件(elementData
),倒退一步,再往回思考下 checkForComodification()
看...
不知讀者老爺有沒恍然大悟,其實很簡單啦: Itr
物件不希望你在使用Itr
迭代器的過程中修改(主要是增刪)ArrayList
中的(elementData
)元素,不然在迭代的時候源陣列少了個元素會直接拋錯
的,Itr
內的expectedModCount
只會在 new Itr()
時被賦值一次,這就是很好的證明啦~
Itr
是Iterator
的實現,裡面只有迭代
的操作,如果有更復雜的操作,比如ListItr
(是Itr
以及ListIterator
的繼承實現) 裡面更是對迭代器增加了增刪改查
方法,以及SubList
這個物件內部也是,內部均是對ArrayList
維護elementData
直接操作(他們並未拷貝elementData
),所以裡面的增刪操作不僅僅要比較ArrayList
的elementData
版本,也要在操作(增刪)之後同步ArrayList
的modCount
版本,以下是ListItr
內的add(E e)
函式的原始碼:
public void add(E e) {
checkForComodification();
try {
// ArrayList的新增方法,在遊標位置新增元素,遊標及之後的元素往後移動,
int i = cursor;
// 這裡還需要注意的是這個插入是在當前元素之後插入元素,ArrayList則是在元素之前,這主要是遊標是當前位置+1
ArrayList.this.add(i, e);
// 因為增加了個元素,所以遊標的位置要+1,當前位置lastRet會在下一次呼叫next或previous時會被重置
cursor = i + 1;
lastRet = -1;
// 這個其實類似於版本的概念,主要由於ArrayList與Iterator修改不同步,expectedModCount,這個會在 checkForComodification() 中進行校驗
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
以上程式碼中有這樣一句 expectedModCount = modCount;
不知讀者們明白否。。。?
ArrayList
中為啥有版本管理,版本管理怎麼用?
好了,我總結下本小節開頭的的兩個問題
首先版本管理就是在增刪元素的時候對 modCount
自增1
因為對ArrayList
的迭代器 Itr
、ListItr
以及SubList
(擷取類) 他們是單獨類物件同時內部也是直接操作的ArrayList
的源elementData
陣列物件,所以在ArrayList
新增元素時這三個類內部方法均不知道陣列元素個數
已發生變化,所以在操作elementData
時候均需要判讀版本
是否一致,這就是為啥有版本;
他解決的是:這幾個類在操作 elementData
(ArrayList
的)時 ArrayList
可能對其的增刪導致的版本
不一致的問題,總結似乎臭長了些,但就是這麼個意思?
理解這個很重要,不然你在讀 ArrayList
的 add
、remove
這類方法時不知modCount
作甚云云,哈哈。。。