ArrayList-ListItr源碼逐條解析

一家之言 姑妄言之 絮絮叨叨 不足爲訓

筆者廢話:

   這篇文章是ArrayList源碼逐條解析外述篇。爲什麼來個外述篇呢?因爲:
   1. 這個類作爲ArrayList的迭代方式是非常重要的
   2. 我是實在不想在“ArrayList的遍歷功能解析”中解析這個類了,本身它是非常重要的,如果不單拿出來講而是放在ArrayList源碼逐條解析這個文章裏解析其實會給人造成誤解認爲其不重要;
   3. 公司裏的領導告訴我源碼分析寫的過於長可能影響觀感。
   所以,我這裏把這個類單拿出來進行解析(>ω<)。


ArrayList-ListItr類註釋翻譯:

   一個優化版的AbstractList.ListItr


ArrayList-ListItr類信息:

private class ListItr extends Itr implements ListIterator<E>

   我們可以清楚的看到,ListItr是一個繼承了Itr類,並且實現了Iterator接口的類。起碼我們從這裏能看出這個ListItr是一個具有遍歷集合屬性的Itr類子類,同時也是Iterator接口實現類。
   那麼關於Itr類,也就是咱們本類的父類解析,可以參考ArrayList-Itr源碼逐條解析這篇文章。而關於Iterator接口的解析我們已經在Iterator源碼逐條解析裏面介紹過了。想要詳細瞭解的話,可以查看這篇Iterator源碼逐條解析文章。


ArrayList-ListItr構造函數信息:

ListItr(int index) {
    super();
    cursor = index;
}

   我們從這個構造函數來看,發現這個是一個有參構造器。其中會傳入一個索引值index。不過,這裏我們會看到第一行會調用父類的構造函數,也就是Itr類。而下方的這個遊標cursor也是父類的屬性。
   另外,我們還會發現,這個ListItr類的構造函數有且只有一個有參構造器,所以,我們在創建ListItr類時會主動向內傳入一個索引index。不過放心,這一步並不用我們去創建,這一步已經被封裝進ArrayList類中的listIterator(int index)方法和listIterator()方法中了。

ArrayList-ListItr成員變量信息:

   這裏的成員變量信息都繼承自了父類Itr類的成員變量,所以如果有疑問,那麼你可以複習一下ArrayList-Itr源碼逐條解析這篇文章裏面所介紹的。


ArrayList-ListItr的方法解析:

   好,我們現在開始進入正題了,別忘了,這個ListItr類不光繼承了Itr類,而且還是對接口ListIterator的具體實現。具體的情形可先閱讀ListIterator源碼逐條解析倒數第二段。
   不過,這裏還需要說明的是,因爲本類繼承了Itr類,所以其中的hasNext()方法、next()方法、remove()方法在這裏都沒有進行覆寫。而這裏出現的則是覆寫了ListIterator接口中的方法。

public boolean hasPrevious() {
    return cursor != 0;
}

   這裏覆寫了ListIterator接口中的hasPrevious()。我們知道這個方法是在詢問當前遍歷的容器中是否含有上一個元素,那麼我們看具體的實現是如何呢?
   cursor != 0,對,就是這樣。它在判斷我們的cursor遊標是否不等於當前數組內第一個元素的索引值0。因爲如果等於了0不就代表我們這個遊標指向已經指向了第一個元素了嗎?那麼它還會有前一個元素嗎?肯定不會的。
   **千萬記住,這裏的hasPrevious()方法調用完後,遊標指向cursor是不會移動的

public int nextIndex() {
    return cursor;
}

public int previousIndex() {
    return cursor - 1;
}

   這裏我們一同介紹上述兩個方法。它們覆寫了ListIterator接口中的nextIndex()previousIndex()。這兩個方法的作用分別是返回當前下一個未遍歷元素的索引index和返回當前下一個未遍歷元素的上一個元素的索引index
   通俗意義上來說,從代碼中我們發現nextIndex()方法返回當前遊標所處的索引,而previousIndex()方法則返回當前最後一次已遍歷出的元素的索引。
   我們來舉個例子:

/**
 * 放心運行
 */
public static void main(String[] args) {
    LinkedList<Integer> arrayList = new LinkedList<Integer>();
    arrayList.add(1);
    arrayList.add(2);
    arrayList.add(3);
    arrayList.add(4);
    arrayList.add(5);
    ListIterator listIterator = arrayList.listIterator();
    System.out.println("當前遍歷出的元素: " + listIterator.next());
    System.out.println("下一個元素的索引: " + listIterator.nextIndex());
    System.out.println("上一個元素的索引: " + listIterator.previousIndex());
}
/*
 * 輸出結果:
 * 當前遍歷出的元素: 1
 * 下一個元素的索引: 1
 * 上一個元素的索引: 0
 */

   我們從這個例子可以看出,當我們遍歷出第一個元素的時(1),這個時候,遊標cursor右移,指向了索引1。也就是說cursor = 1。而這個索引1指向的元素是數字2。
   這個時候我們調用nextIndex()方法,返回當前遊標cursor就會返回索引1。然而,這個索引1所代表的元素2我們還沒有進行遍歷。
   那麼我們調用previousIndex()方法,返回當前遊標cursor - 1就會返回索引0。而這個索引0所代表的則是我們剛剛遍歷出的元素1。
   這也印證了我們之前說的,nextIndex()方法返回當前下一個未遍歷元素的索引indexpreviousIndex()返回當前最後一次已遍歷出的元素的索引index

@SuppressWarnings("unchecked")
public E previous() {
    checkForComodification();
    int i = cursor - 1;
    if (i < 0)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i;
    return (E) elementData[lastRet = i];
}

   這裏是迭代的重要步驟,這個previous()纔是真正可以取出元素的方法。它與next()方法的含義是相反的。next()方法是遍歷下一個,而我們的previous()方法則是遍歷上一個。我們經過了之前的hasPrevious()方法判斷是否含有上一個元素之後,就可以緊接着調用該方法。
   我們來看這個方法的具體實現。
   第一步,先利用checkForComodification()方法對修改值modCount進行檢查,判斷當前數組是否發生修改。
   第二步,聲明一個局部變量i,並將當前遊標值的前一位cursor -1賦予它(其實這裏就是遊標本身的左側一位,我們接下來描述的時候就用“遊標”這個概念)。

筆者廢話:

   我們可以試想一下,next()是將cursor賦予i是因爲我們需要遍歷的是下一個元素,也就是進行移位後的cursor所指向的那個元素。而previous()需要遍歷的是遊標cursor指向的上一個元素,所以這裏用cursor -1賦予i是邏輯通暢的。不過這裏也體現了閱讀源碼的重要性,這個previous()方法其實返回就是你最後一次已經遍歷出來的那個元素
   第三步,判斷遊標是否小於當前數組第一個元素的索引0,如果符合判斷條件,則拋出NoSuchElementException異常。這點還是易於理解的,之前我們說過,當這個遊標cursor的值等於了當前數組第一個元素的索引0的話就已經代表我們遍歷到了數組的第一個元素,何況還要cursor - 1。這樣就會遍歷第一個元素的左側那個元素。有嗎?肯定沒有的。因爲這裏數組腳標越界了,所以這個時候拋出異常就不足爲奇了。
   第四步,是將當前ArrayList的數組賦予一個新的類型爲Object的數組,其實就是把當前的數組複製了一份;
   第五步,到了這裏,你看,又一次把遊標進行判斷,判斷什麼呢?判斷我們的遊標是否大於或等於了數組的容量length,如果符合判斷條件,則拋出ConcurrentModificationException異常。看,我們這裏又有一個熟悉的異常,那麼爲什麼遊標大於或等於了數組的容量length就會拋出這個異常呢?其實,它還是在做檢測,檢測集合是否被修改過。

筆者廢話:

   到這裏你仔細想想,我們假設元素個數size就是數組長度length,也就是說這個數組已經被填滿了。但是這個時候,你通過了第三步的數組腳標越界的判斷,但是沒有通過第五步的容量判斷,也就是發生了這種場景:length <= i > 0。這種情況可能嗎?數組容量小於了當前的索引位置?這種情況有可能,就是你刪除了元素,然後你的數組縮容了。說的直白一些當你remove()完畢後你又調用了類似於trimToSize()的縮容方法。這可不就是修改了集合元素嘛~所以,這個時候拋出ConcurrentModificationException異常是一種非常正確的行爲。
   第六步,這個時候,該判斷的也都判斷了,我們可以正常的取出元素返回給調用者了。不過這裏我們需要開始最重要的一步了,挪動我們的遊標卡尺。形如代碼所說的那樣,當前遊標變爲i即可。也就是這個遊標指向向左移動一格。

筆者廢話:

   這裏可能有些繞。也就是遊標左移cursor - 1我能明白,但是這個當前遊標cursor也左移是爲了什麼?其實這裏ListIterator源碼逐條解析已經解釋的很清楚了。next()方法和previous()方法上的註釋說的很清楚了,如果這兩個方法交替使用,將返回同一個元素。如何達到這種目的呢?那就是當我們進行了previous()方法返回了最後一次遍歷的那個元素後,只有當前的cursor也進行一次左移,纔會在下一次next()方法返回這個相同的元素。形似這樣:

1 2 3 4 5

   當我們需要遍歷上述的元素時,如果調用了兩次next()方法,這個時候會返回給我們數字2。而當前遊標cursor則如下指向:

1 2 3 4 5
cursor ^

   而當我們調用一次previous()方法,這個時候會返回最後一次遍歷的元素2,
這個毋庸置疑,但是這個時候我們上方代碼的i,也就是cursor - 1指向了哪裏呢?指向瞭如下的位置:

1 2 3 4 5
i ^
cursor ^

   那麼這個時候,如果我們還想利用ListIterator接口所謂的返回相同元素的特性,你覺得我們在調用next()方法的時候這個遊標cursor會指向哪裏呢?那麼只有下面這樣,纔會在調用next()方法的時候返回previous()返回的那個元素。也就是所謂“相同的元素”:

1 2 3 4 5
i ^
cursor ^

   到這裏你看,是不是需要執行cursor = i這個操作很有必要呢?答案是肯定的。
   第七步,返回所遍歷的元素,返回哪個呢?當然返回我們之前想要遍歷的那個遊標所在處左側的元素,也就是那個i,也就是返回數組當前i索引處的元素。
   但是,這裏又碰到了一個問題,那就是在next()方法內,返回操作中的步驟是lastRet = i。而這裏的previous()方法內的返回操作中也是lastRet = i。細心的人會覺得這裏的lastRet難道不是代表遊標cursor左側的那個元素嗎?返回i處的元素是沒有問題的,但是你這裏的lastRet和上面分析得出的cursor不就重疊了嗎?
   能想到這裏我真的非常高興!!!我也不賣關子了,直接說結論:這裏的lastRet = i操作,是爲了remove()set(E e)這個方法的。當然,我們這樣指出來可能是不對的,但是通過源碼,這裏的兩個方法都依靠着lastRet這個值進行處理,所以我這裏提出的觀點可能不是很對,但是當你面試的時候說出了這個要更好一些~
   那麼我們仔細想一下remove()方法刪除的是哪個位置的元素?是lastRet這個位置的元素。千萬不要忘了,ListIterator接口中對remove()方法的定義:

從列表中刪除next或previous返回的最後一個元素

   所以,你應該就明白了,這個lastRet = i操作是必須的,因爲當我想要刪除由previous()方法返回來的這個元素的時候,我的lastRet必須指向這個元素(*•̀ᴗ•́*)و ̑̑
   另外,還有這個set(E e)方法,但是因爲在下面我們會解析到這個源碼,所以這裏就不做解釋。看我的解析就可以了~
   到此,previous()方法解析完成~

public void set(E e) {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();
    try {
        ArrayList.this.set(lastRet, e);
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

   我們接下來的這個方法也是覆寫了ListIterator接口中的set(E e)方法。這個方法我們在ListIterator源碼逐條解析中簡單介紹過。其目的是爲了修改調用next()方法和previous()方法返回的元素。我們一會兒舉例,先來分析源碼:
   第一步,首先會判斷我們的lastRet是否小於0。如果小於的話就拋出IllegalStateException異常。這步比較好理解,你都小於0了我修改誰去啊?不過正經的說,根據這個方法的本意是爲了修改調用next()方法和previous()方法返回的元素。那麼針對於next()方法是修改遊標cursor的前一項,即lastRet指向的那一項。而針對於previous()方法則是修改當前cursor指向的那一項,其實還是lastRet指向的那一項(這一點參考previous()方法解析)。
   第二步,利用checkForComodification()方法對修改值modCount進行檢查,判斷當前數組是否發生修改。
   第三步,利用ArrayList類本身的set(int index, E element)方法對我們的元素進行修改。你看,這裏傳入的第一個索引值就是我們的lastRet,也就是說修改的就是這個lastRet指向的那一項。當然,這裏如果出現了異常,則拋出ConcurrentModificationException異常。
   我們來舉兩個例子:
   情形一:next()方法的調用:

/**
 * 正確實例,放心運行
 */
public static void main(String[] args) {
    ArrayList<Integer> arrayList = new ArrayList<>();
    arrayList.add(1);
    arrayList.add(2);
    arrayList.add(3);
    arrayList.add(4);
    arrayList.add(5);
    ListIterator listIterator = arrayList.listIterator();
    listIterator.next();
    listIterator.next();
    listIterator.set(10);
    System.out.println(arrayList);
}
/*
 * 輸出結果:
 * [1, 10, 3, 4, 5]
 */

   情形二:previous()方法的調用:

/**
 * 正確實例,放心運行
 */
public static void main(String[] args) {
    ArrayList<Integer> arrayList = new ArrayList<>();
    arrayList.add(1);
    arrayList.add(2);
    arrayList.add(3);
    arrayList.add(4);
    arrayList.add(5);
    ListIterator listIterator = arrayList.listIterator();
    listIterator.next();
    listIterator.next();
    listIterator.previous();
    listIterator.set(10);
    System.out.println(arrayList);
}
/*
 * 輸出結果:
 * [1, 10, 3, 4, 5]
 */

   我們發現這兩個示例的輸出結果都一樣,其實這個正常的。在情形一中,我們調用了兩次next()方法之後,遊標cursor指向了索引2lastRet指向了索引1,所以這裏在第二個元素進行修改是無誤的。那麼在情形二中,我們調用了兩次next()方法之後又調用了一次previous()方法,這個時候雖說遊標cursor指向了索引1,但是lastRet指向的也是索引1。所以這裏在第二個元素進行修改也是無誤的。
   這樣的話,我們就對這個set(E e)方法理解的就更深了。

public void add(E e) {
    checkForComodification();
    try {
        int i = cursor;
        ArrayList.this.add(i, e);
        cursor = i + 1;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

   那麼最後這一個方法也是覆寫了ListIterator接口中的add(E e)方法。之前我們在ListIterator源碼逐條解析中說過這個方法,它大致描述爲插入更加合適。其目的是在調用了next()方法或previous()方法返回的元素之後插入指定元素。
   我們還是先來看源碼,然後再舉例。
   第一步,依舊是利用checkForComodification()方法對修改值modCount進行檢查,判斷當前數組是否發生修改。
   第二步,獲取當前的遊標cursor並賦予新聲明的變量i
   第三步,這裏是極爲重要的一步,就是插入動作,你也可以叫添加。它調用了ArrayList本類的add(int index, E element)方法來進行插入操作。第二步中的i就是插入的位置索引。通俗的說就是,在遊標cursor處進行插入操作。
   第四步,將當前的遊標cursor進行加1操作。這步操作的目的是爲了不影響遊標cursor所代表的含義,即,指向某一個特定元素。你在這個位置新添加了一個元素,那麼我當初遊標cursor如果還在這個位置的話,是不是就指向了這個新元素?但是其本身是要指向原先你本身cursor指向的那個舊元素,怎麼辦呢?你就只能進行移位操作,也就是加1了
   第五步,我們把這個上一項元素指向lastRet初始化爲-1。這種操作無非就是不讓你進行set(E e)操作,抑或是不讓你進行remove()操作。爲什麼呢?因爲其實現的方法註釋是規定了如此:

remove()規定:
只有在最後一次調用next或previous之後沒有調用add時,纔可以執行此操作。
set(E e)規定
只有在最後一次調用next或previous之後既不調用remove也不調用add,纔可以執行此調用。

   第六步,同步操作值modCountexceptModCount,這個就不須過多的解釋了,因爲add(i, e)操作可能會使得當前ArrayList的操作值modCount加1,所以這裏爲了避免ConcurrentModificationException異常你需要把這個修改了的值更新到我們ListItr中的期望修改值exceptModCount內。
   我們接下來來看兩個示例:
   情形一:next()方法的調用:

/**
 * 正確實例,放心運行
 */
public static void main(String[] args) {
    ArrayList<Integer> arrayList = new ArrayList<>();
    arrayList.add(1);
    arrayList.add(2);
    arrayList.add(3);
    arrayList.add(4);
    arrayList.add(5);
    ListIterator listIterator = arrayList.listIterator();
    listIterator.next();
    listIterator.next();
    listIterator.add(10);
    System.out.println(arrayList);
}
/*
 * 輸出結果:
 * [1, 2, 10, 3, 4, 5]
 */

   情形二:previous()方法的調用:

/**
 * 正確實例,放心運行
 */
public static void main(String[] args) {
    ArrayList<Integer> arrayList = new ArrayList<>();
    arrayList.add(1);
    arrayList.add(2);
    arrayList.add(3);
    arrayList.add(4);
    arrayList.add(5);
    ListIterator listIterator = arrayList.listIterator();
    listIterator.next();
    listIterator.next();
    listIterator.previous();
    listIterator.add(10);
    System.out.println(arrayList);
}
/*
 * 輸出結果:
 * [1, 10, 2, 3, 4, 5]
 */

   上面這兩個示例輸出就過不太一樣,其實這個正常的。
   在情形一中,我們調用了兩次next()方法之後,遊標cursor指向了索引2,這個時候你進行添加是往索引2這個地方進行添加,也就是擠佔了原本數字3的位置。所以這裏將3以及以後的所有元素後移一位,然後在本身數字3這個位置變更爲10即可。
   那麼在情形二中,我們調用了兩次next()方法之後又調用了一次previous()方法,這個時候遊標cursor指向了索引1,也就是擠佔了原本數字2的位置。所以這裏將2以及以後的所有元素後移一位,然後在本身數字2這個位置變更爲10即可。
   這樣的話,我們就對這個add(E e)方法理解的就更深了。
   至此,我們ArrayList-ListItr到此全部解析完畢(ಥ_ಥ)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章