ArrayList源碼逐條解析(續)

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

筆者廢話:

   這篇文章是ArrayList源碼逐條解析續篇。要想讀通這篇文章,請一定要仔細閱讀ArrayList源碼逐條解析這篇文章。
   這裏解釋一下爲什麼來個續篇呢?因爲:上篇文章篇幅過大,而且這個markdown編輯器因爲篇幅問題已經無法正常響應了,所以就需要另起這篇文章進行解析。
   好,我們現在開始吧(>ω<)。

ArrayList的截取方法的解析:

/**
 * @param fromIndex 起始位置
 * @param toIndex 結束位置
 * @param size 元素個數
 */ 
static void subListRangeCheck(int fromIndex, int toIndex, int size) {
    if (fromIndex < 0)
        throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
    if (toIndex > size)
        throw new IndexOutOfBoundsException("toIndex = " + toIndex);
    if (fromIndex > toIndex)
        throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                           ") > toIndex(" + toIndex + ")");
}

   我們在介紹ArrayList的截取方法之前,先介紹這一個方法。這是一個範圍檢查方法。目的很直接,就是判斷我們我們截取元素的起始位置和結束位置是否符合規範。也是代碼健壯性的一種體現。那麼,我們來看看這個方法是一個什麼步驟。
   第一步,先判斷我們的起始位置fromIndex是否小於0。很簡單,數組位置小於0肯定是不合規的。所以這個時候就拋出了IndexOutOfBoundsException異常,並通知開發者這個起始位置是哪裏。
   第二步,然後判斷我們的結束位置toindex是否大於元素個數size。這點是因爲我們大多數的截取方法都是左閉右開區間,形如這樣:[1,5)。所以索引爲size處的元素其實也只能取到最後一位。那麼這一步就和第一步類似了,檢查數組位置是否合規。如果不合規就拋出IndexOutOfBoundsException異常,並通知開發者這個結束位置是哪裏。
   第三步,上述判斷都合理了還差一步範圍是否合理,不可能存在起始位置比結束位置還要大的現象,例如[3,-1)這個區間你怎麼取?所以這裏還要判斷一下起始位置是否大於了結束位置。如果大於了,則拋出IllegalArgumentException異常,並通知開發者哪個起始位置大於了哪個結束位置。
   我們需要注意的是,這個方法被static修飾,也是就代表這個方法是一個類方法。另外,它並沒有特定的權限修飾符,也就是默認爲default修飾,所以這個方法也只能在與ArrayList類的同一個類內同一個包內進行訪問。

/**
 * 返回指定的fromIndex(包含)和toIndex(不包含)之間的列表部分的視圖。
 * (如果fromIndex和toIndex相等,則返回的列表爲空。)返回的列表由這
 * 個列表支持,因此返回列表中的非結構性更改將反映在這個列表中,反之亦然。
 * 返回的列表支持所有可選的列表操作。這種方法不需要顯式的範圍操作(數組中
 * 通常存在的排序)。通過傳遞子列表視圖而不是整個列表,任何期望列表的操作
 * 都可以用作範圍操作。例如,下面的習慣用法從列表中刪除一系列元素:
 * 			list.subList(from, to).clear();
 * 可以爲indexOf(Object)和lastIndexOf(Object)構造類似的習慣用法,
 * 而Collections類中的所有算法都可以應用於一個子列表。該方法返回的列表
 * 的語義將變得未定義,如果支持列表(即此列表)除通過返回的列表外,以任何
 * 方式對其進行結構修改。(結構修改是指改變列表的大小,或者以一種正在進行
 * 的迭代可能產生錯誤結果的方式擾亂列表。)
 * 
 * @param fromIndex 子列表的低端(包括)
 * @param toIndex 子列表的高端(不含)
 * 
 * @throws 如果出現用於非法端點索引值(fromIndex<0||toIndex>size
 * ||fromIndex>toIndex),則拋出IndexOutOfBoundsException異常
 * @throws 如果端點索引無序(fromIndex>toIndex),則拋出
 * IllegalArgumentException異常
 */
public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

   接下來我們就進入了正題,介紹ArrayList的截取方法解析。事實上,我們如果從源碼解析的角度來看的話,這個方法的代碼行數也就兩行。當然,我們確實就解釋這兩行而已~具體細節,我們如下說。
   首先,它會引用我們的subListRangeCheck(int fromIndex, int toIndex, int size)方法進行索引判斷,以確定我們想要獲取的列表範圍是否合理。
   其次,會創建並返回一個ArrayList類的私有類SubList類,向這個類的有參構造器傳入當前對象,偏移量0,開始位置和結束位置。
   那麼到此就結束了,並不是不講實現細節,因爲關於這個SubList類我們會挪到另一篇文章裏面進行講解。既然是一個新接觸的類,我們就需要給他另開一篇解析。所以不要着急,參考文章即可。
   其實到這裏,我們還是說一下這個方法的目的吧。這個方法目的就是爲了返回指定範圍內,當前ArrayList的自列表,而指定範圍左閉右開。
   需要注意的是,獲取的這個“子列表”僅僅就是一個“子視圖”而已,這意味着,如果我們修改了當前獲取的子列表,那麼當前的父列表,也就是當前的ArrayList內的元素也會進行修改。這也就是註釋中:“返回列表中的非結構性更改將反映在這個列表中”。
   我們來舉個例子:

/**
 * 正確示例,放心運行
 */
public static void main(String[] args) {
    ArrayList<Integer> arrayList = Lists.newArrayList(1, 2, 3, 4, 5);
    List subList = arrayList.subList(0, 4);
    System.out.println(subList);
    subList.set(1, 10);
    System.out.println(subList);
    System.out.println(arrayList);
}
/* 
 * 運行結果:
 * [1, 2, 3, 4]
 * [1, 10, 3, 4]
 * [1, 10, 3, 4, 5]
 */

   你會發現我們創建了一個含有5個元素的列表。現在我們獲取其中的前四個,也就是左閉右開[0,4)。這個時候我們就得到了子列表subList,我們能確定的是,這個子列表的元素是列表[1, 2, 3, 4]。這樣,我們修改其中第二個參數爲10,看看什麼效果。
   通過上述的結果我們發現,子列表subList變爲了列表[1, 10, 3, 4]。最重要的是,我們原有的arrayList內的元素變爲了列表[1, 10, 3, 4, 5]。這意味着當我們修改子列表的時候,其父列表也會跟着變動。
   所以說,如果我們採用這種方式獲取了子列表,一般來說是不建議進行修改內部元素的。通常情況下,我們拿到固定的數據後僅僅進行非增、刪、該這種操作的業務就可以了。

@Override
public boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    // 在此階段,從篩選器謂詞拋出的任何異常都將使集合保持不變
    int removeCount = 0;
    final BitSet removeSet = new BitSet(size);
    final int expectedModCount = modCount;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        @SuppressWarnings("unchecked")
        final E element = (E) elementData[i];
        if (filter.test(element)) {
            removeSet.set(i);
            removeCount++;
        }
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    // 將剩餘的元素移動到被移除元素所留下的空間上
    final boolean anyToRemove = removeCount > 0;
    if (anyToRemove) {
        final int newSize = size - removeCount;
        for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
            i = removeSet.nextClearBit(i);
            elementData[j] = elementData[i];
        }
        for (int k=newSize; k < size; k++) {
            elementData[k] = null;  // Let gc do its work
        }
        this.size = newSize;
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }
    return anyToRemove;
}

   我們這裏介紹一個刪除方法,這是一個具有函數式編程特性的方法。其參數是傳入一個Predicate,這個單詞翻譯成謂語,不過我們更喜歡把它翻譯成過濾條件。也就是說我們可以自擬定刪除條件,符合過濾條件的元素可以被刪除。
   其實這個方法本應該在ArrayList源碼逐條解析這篇文章的“”模塊下進行解析介紹。不過因爲其具有函數式編程特性,所以在這裏就單獨的介紹出來。
   我們先來看一下源碼,然後再舉一個例子。
   第一步,先來判定我們的過濾條件是否爲空。如果爲空的話就拋出NullPointerException異常。
   第二步,這裏有一個單行註釋,也就是從這個步驟往下開始,從過濾條件拋出的任何異常都將使集合保持不變。
   第三步,聲明一個計數器removeCount,用來記錄刪除元素的個數。
   第四步,聲明一個BitSet用來存儲二進制位類型的對象,命名爲removeSet
   第五步,將當前ArrayList的操作值modCount賦予新聲明的預期修改值exceptModCount。同時獲取當前數組的元素個數,並定義其爲final不可修改的狀態。
   第六步,開始遍歷整個ArrayList元素。我們先說一下這裏到底幹了什麼,這裏其實就是拿我們的過濾條件對各個元素進行檢測。假如不通過,那就遍歷下一個元素。假如通過了,那麼就向這個BitSet內傳入這個元素的索引並將計數器removeCount進行加1操作。其實我們到時候刪除的就是這個BitSet裏面記錄索引所對應的元素。這裏需要注意一點的是,遍歷的終止條件是操作值modCount和預期修改值exceptModCount不相等。
   這步可記住了,你沒刪除元素,你根本就沒有刪除元素。元素一直都在。只不過是在BitSet裏面進行了區分,要刪除的標記爲true,不刪除的標記爲false
   第七步,對操作值modCount和預期修改值exceptModCount進行判斷,如果不符合預期條件(也就是不相等),就拋出ConcurrentModificationException異常。
   第八步,從這裏開始,纔是真正的刪除元素。說是“刪除”,其實就是挪動元素。這裏首先會進行一次判斷,問你你的計數器removeCount是否大於0。也就是說是否有可刪除的元素。這個計數器removeCount是在第六步的時候已經計算好了的。
   第九步,我們通過了判斷,再次進行計算。你看,這裏面有一步size - removeCount操作。這其實是在計算我們進行刪除操作後新數組的元素個數newSize
   第十步,這裏是進行了一次遍歷,我們先解釋一下這個遍歷的效果是什麼。這裏進行遍歷其實就是對原始數組中的非刪除元素進行保留操作。那麼這裏我們聲明瞭兩個索引值,一個是i,另一個是j。索引i用於標定原始數組,索引j標定新數組。其中遍歷的通過條件是原始數組的索引不得大於原始數組的元素個數同時新數組的索引不得大於新計算出來的元素個數
   第十一步,在第十步之後獲取removeSet中標記爲false的原始數組索引。然後將這些索引對應的元素挪動到新數組中。再次強調的是,這裏的操作是一種保留
   第十二步,經過保留完畢後,這時候,我們纔開始所謂的“刪除”操作。這個時候我們開始遍歷新數組,其實位置就是我們新計算的新數組的元素個數newSize。這裏還是需要提示一點的是索引是從下標0開始的,所以newSize所對應就是新數組最後一個元素的後一位,從這一位開始,之後的元素都不再要。
   第十三步,既然都不要,那麼這個新數組打從newSize這個位置開始,其後的所有元素全部置爲null這樣,整個刪除步驟就完畢了
   第十四步,開始一些掃尾工作,重新更新當前ArrayList數組的元素個數,將newSize賦予size。然後判斷操作值modCount和預期修改值exceptModCount是否不相等。最重要的則是判斷通過之後,對操作值modCount進行加1操作。
   最後,返回anyToRemove來告知調用者是否進行了指定過濾條件的刪除操作。而這個值則是由我們第九步removeCount > 0計算出來的。
   這裏我們來例舉一個示例:

/**
 * 正確示例,放心運行
 */
public static void main(String[] args) {
    ArrayList<Integer> arrayList = Lists.newArrayList(1, 10, 2, 3, 5);
    arrayList.removeIf(s -> s > 3);
    System.out.println(arrayList);
}
/*
 * 運行結果:
 * [1, 2, 3]
 */

   上述這個示例表示,我們需要刪除集合中大於3的元素。你看,在removeIf(Predicate<? super E> filter)方法內,填入我們的過濾條件就可以了~這種操作非常適合我們的業務查詢篩選操作。一旦我們獲取了一部分數據,但是還需要從這些數據裏面進行篩選的話,利用這個操作就可以把無用的數據刪除,從而保留我們想要的結果。

@Override
@SuppressWarnings("unchecked")
public void replaceAll(UnaryOperator<E> operator) {
    Objects.requireNonNull(operator);
    final int expectedModCount = modCount;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        elementData[i] = operator.apply((E) elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    modCount++;
}

   我們這裏再介紹一個替換方法,這也是一個具有函數式編程特性的方法。其參數是傳入一個UnaryOperator,這個單詞翻譯成一元操作,不過我們更喜歡把它翻譯成處理業務。也就是說我們可以在這裏傳入一組對數組內元素進行操作的業務處理邏輯,然後將處理後的元素再一次返回回來。
   我們還是先來看一下源碼,然後再舉一個例子。
   第一步,先來判定我們的過濾條件是否爲空。如果爲空的話就拋出NullPointerException異常。
   第二步,同步我們當前ArrayList的操作數modCount到新聲明的預期修改值exceptModCount上。
   第三步,同步我們當前ArrayList的元素個數size。這裏的元素個數size與上一步的預期修改值exceptModCount都是final修飾的,也就是不可變的。
   第四步,開始進行遍歷。這個一步操作其實是可以預見的,因爲說要對所有元素進行業務處理,所以肯定會有一次遍歷。那麼遍歷都幹嘛呢?我們看下面。
   第五步,到這裏已經進入了遍歷體內,我們發現它會對每一個元素進行operator.apply((E) elementData[i])操作。而這一步操作就我們當初傳入的業務處理邏輯。當我們處理完畢後,又把這個元素重新賦予當前這個索引位置上的元素,也就是替換了當前的元素
   第六步,還是老樣子,對操作值modCount和預期修改值exceptModCount是否不相等做判斷,如果不相等則拋出ConcurrentModificationException異常。
   最後,對操作值modCount進行加1操作就可以了。
   這裏我們來例舉一個示例:

/**
 * 正確示例,放心運行
 */
public static void main(String[] args) {
    ArrayList<String> arrayList = Lists.newArrayList("對象1", "對象2", "對象3", "對象4", "對象5");
    arrayList.replaceAll((s) -> {
        s = "對\"" + s + "\"進行操作";
        return s;
    });
    System.out.println(arrayList);
}
/*
 * 運行結果:
 * [對"對象1"進行操作, 對"對象2"進行操作, 對"對象3"進行操作, 對"對象4"進行操作, 對"對象5"進行操作]
 */

   這個示例的目的其實很明確了,就不做具體解釋了。不過我們可以通過這個示例代碼聯想一下這個replaceAll(UnaryOperator<E> operator)方法的應用場景。我這裏聯想到了兩個:
   場景一:聯想到我們函數式編程的含義,我們其實可以在這裏進行一些業務處理。譬如我們對獲取到的列表數據做統一的配貨操作。就像我們示例那樣,我們拿過數據來對集合內的數據進行操作,然後再返回這些操作結果以供上層使用。
   其實這裏是省去了一些步驟。你看,一般我們獲取到數據後(我們暫且稱之爲原始數據),我們會對這些原始數據做操作。這裏不可避免的需要進行遍歷。但是你遍歷完畢後,處理完的元素你也得找個地方存起來,然後返回給調用者。那麼這裏其實你執行了4步:遍歷—>處理—>封裝—>返回。但是,replaceAll(UnaryOperator<E> operator)方法卻只讓你執行了一步——處理即可。
   場景二:這裏也是場景一的一個衍生吧(對,不是延伸)。
   還記得我們寫的SQL語句查詢嗎?還記得我們可能隨手就來一個DATE_FORMAT嗎?還記得我們可能隨手就來一個CONCAT嗎?還記得…
   對,我們可以不用這些函數來進行字段處理了。一般我們從數據庫獲取數據的時候首先會對數據進行處理,類似於日期格式化,類似於字符串拼接…但是,如果語句複雜的話這種操作是要耗費數據庫性能的。所以,我們可以不用這些方法,直接返回最原始的數據。把數據庫計算的壓力拋給我們的Java程序,讓Java程序進行處理(Java表示壓力不大
   以上,就是我能想到的兩種應用場景。

@Override
@SuppressWarnings("unchecked")
public void sort(Comparator<? super E> c) {
    final int expectedModCount = modCount;
    Arrays.sort((E[]) elementData, 0, size, c);
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    modCount++;
}

   最後,這真的是最後,我們最後開始介紹我們ArrayList類內的最後一個方法——sort(Comparator<? super E> c)。本身最後一個方法可能需要更加儀式感的去講解。但是,奈何這個方法不爭氣,它就是一個帶有函數式編程接口參數的一個排序方法。
   我們還是先來看源碼,然後舉例子
   第一步,這步有取。你看,這裏竟然不進行判空操作了。也就是說明,我們這個比較器(參數c)是可以爲空的。
   第二步,同步我們當前ArrayList的操作數modCount到新聲明的預期修改值exceptModCount上,並將其定義爲final修飾,意味着它不可變。
   第三步,最重要的一步。調用Arrays類中的排序方法對整個ArrayList數組進行排序。而比較器則取自我們傳入的c。這裏我們並不對這個Arrays類中的排序方法做詳細介紹,因爲我會在另一篇文章Arrays源碼逐條解析裏面介紹它。
   第四步,對操作值modCount和預期修改值exceptModCount是否不相等做判斷,如果不相等則拋出ConcurrentModificationException異常。
   最後,對操作值modCount進行加1操作就可以了。
   這裏我們來例舉幾個示例:

/**
 * 示例一,正確示例,放心運行
 */
public static void main(String[] args) {
    ArrayList<String> arrayList = Lists.newArrayList("對象5", "對象2", "對象3", "對象4", "對象1");
    arrayList.sort(null);
    System.out.println(arrayList);
}
/*
 * 運行結果:升序排序
 * [對象1, 對象2, 對象3, 對象4, 對象5]
 */

/**
 * 示例二,正確示例,放心運行
 */
public static void main(String[] args) {
    ArrayList<String> arrayList = Lists.newArrayList("對象5", "對象2", "對象3", "對象4", "對象1");
    arrayList.sort((a, b) -> {
        return a.compareTo(b);
    });
    System.out.println(arrayList);
}
/*
 * 運行結果:升序排序
 * [對象1, 對象2, 對象3, 對象4, 對象5]
 */

/**
 * 示例三,正確示例,放心運行
 */
public static void main(String[] args) {
    ArrayList<String> arrayList = Lists.newArrayList("對象5", "對象2", "對象3", "對象4", "對象1");
    arrayList.sort((a, b) -> {
        return b.compareTo(a);
    });
    System.out.println(arrayList);
}
/*
 * 運行結果:降序排序
 * [對象5, 對象4, 對象3, 對象2, 對象1]
 */

   我們這裏一連舉了三個示例,其中並沒有對Lambda表達式做優化,是因爲我希望這個對比效果要強烈些。不過這裏都是在進行排序。
   我們來看第一個示例,我們根據源碼解析的提示,這裏的比較器傳入了null。那麼得出的結果是一個升序排序的結果,也就是說如果我們不規定比較條件,那麼返回的結果默認爲升序排序。
   接下來我們一同說明第二個和第三個示例,因爲這兩個示例具有代表性。他們分別表示了升序排序和降序排序。例子還是那個例子,我們指出結論。
   當我們對數組內的元素排序時,如果想要升序排序,那麼就用第一個參數與第二個參數作比較,形似:a.compareTo(b);而如果想要降序排序,那麼就用第二個參數與第一個參數作比較,形似:b.compareTo(a)。也就是說,想要升,正着來,想要降,倒着來


   那麼到此,我們的ArrayList源碼逐條解析的續篇到此結束。
   同時,我們的ArrayList類,統共1469行代碼就全部解析完畢,謝謝觀看(ಥ_ಥ)。

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