爲何阿里巴巴要求謹慎使用ArrayList中的subList方法

GitHub 3.7k Star 的Java工程師成神之路 ,不來了解一下嗎?html

GitHub 3.7k Star 的Java工程師成神之路 ,真的不來了解一下嗎?java

GitHub 3.7k Star 的Java工程師成神之路 ,真的肯定不來了解一下嗎?git

集合是Java開發平常開發中常常會使用到的。在以前的一些文章中,咱們介紹過一些關於使用集合類應該注意的事項,如《爲何阿里巴巴禁止在 foreach 循環裏進行元素的 remove/add 操做》、《爲何阿里巴巴建議集合初始化時,指定集合容量大小》等。github

關於集合類,《阿里巴巴Java開發手冊》中其實還有另一個規定:併發

-w1379

本文就來分析一下爲何會有如此建議?其背後的原理是什麼?app

subList

subList是List接口中定義的一個方法,該方法主要用於返回一個集合中的一段、能夠理解爲截取一個集合中的部分元素,他的返回值也是一個List。函數

如如下代碼:ui

public static void main(String[] args) {
    List<String> names = new ArrayList<String>() {{
        add("Hollis");
        add("hollischuang");
        add("H");
    }};

    List subList = names.subList(0, 1);
    System.out.println(subList);
}
複製代碼

以上代碼輸出結果爲:this

[Hollis]
複製代碼

若是咱們改動下代碼,將subList的返回值強轉成ArrayList試一下:spa

public static void main(String[] args) {
    List<String> names = new ArrayList<String>() {{
        add("Hollis");
        add("hollischuang");
        add("H");
    }};

    ArrayList subList = names.subList(0, 1);
    System.out.println(subList);
}
複製代碼

以上代碼將拋出異常:

java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList
複製代碼

不僅是強轉成ArrayList會報錯,強轉成LinkedList、Vector等List的實現類一樣也都會報錯。

那麼,爲何會發生這樣的報錯呢?咱們接下來深刻分析一下。

底層原理

首先,咱們看下subList方法給咱們返回的List究竟是個什麼東西,這一點在JDK源碼中註釋是這樣說的:

Returns a view of the portion of this list between the specifiedfromIndex, inclusive, and toIndex, exclusive.

也就是說subList 返回是一個視圖,那麼什麼叫作視圖呢?

咱們看下subList的源碼:

public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}
複製代碼

這個方法返回了一個SubList,這個類是ArrayList中的一個內部類。

SubList這個類中單獨定義了set、get、size、add、remove等方法。

當咱們調用subList方法的時候,會經過調用SubList的構造函數建立一個SubList,那麼看下這個構造函數作了哪些事情:

SubList(AbstractList<E> parent,
            int offset, int fromIndex, int toIndex) {
    this.parent = parent;
    this.parentOffset = fromIndex;
    this.offset = offset + fromIndex;
    this.size = toIndex - fromIndex;
    this.modCount = ArrayList.this.modCount;
}
複製代碼

能夠看到,這個構造函數中把原來的List以及該List中的部分屬性直接賦值給本身的一些屬性了。

也就是說,SubList並無從新建立一個List,而是直接引用了原有的List(返回了父類的視圖),只是指定了一下他要使用的元素的範圍而已(從fromIndex(包含),到toIndex(不包含))。

因此,爲何不能講subList方法獲得的集合直接轉換成ArrayList呢?由於SubList只是ArrayList的內部類,他們之間並無集成關係,故沒法直接進行強制類型轉換。

視圖有什麼問題

前面經過查看源碼,咱們知道,subList()方法並無從新建立一個ArrayList,而是返回了一個ArrayList的內部類——SubList。

這個SubList是ArrayList的一個視圖。

那麼,這個視圖又會帶來什麼問題呢?咱們須要簡單寫幾段代碼看一下。

一、非結構性改變SubList

public static void main(String[] args) {
    List<String> sourceList = new ArrayList<String>() {{
        add("H");
        add("O");
        add("L");
        add("L");
        add("I");
        add("S");
    }};

    List subList = sourceList.subList(2, 5);

    System.out.println("sourceList : " + sourceList);
    System.out.println("sourceList.subList(2, 5) 獲得List :");
    System.out.println("subList : " + subList);

    subList.set(1, "666");

    System.out.println("subList.set(3,666) 獲得List :");
    System.out.println("subList : " + subList);
    System.out.println("sourceList : " + sourceList);

}
複製代碼

獲得結果:

sourceList : [H, O, L, L, I, S]
sourceList.subList(2, 5) 獲得List :
subList : [L, L, I]
subList.set(3,666) 獲得List :
subList : [L, 666, I]
sourceList : [H, O, L, 666, I, S]
複製代碼

當咱們嘗試經過set方法,改變subList中某個元素的值得時候,咱們發現,原來的那個List中對應元素的值也發生了改變。

同理,若是咱們使用一樣的方法,對sourceList中的某個元素進行修改,那麼subList中對應的值也會發生改變。讀者能夠自行嘗試一下。

一、結構性改變SubList

public static void main(String[] args) {
    List<String> sourceList = new ArrayList<String>() {{
        add("H");
        add("O");
        add("L");
        add("L");
        add("I");
        add("S");
    }};

    List subList = sourceList.subList(2, 5);

    System.out.println("sourceList : " + sourceList);
    System.out.println("sourceList.subList(2, 5) 獲得List :");
    System.out.println("subList : " + subList);

    subList.add("666");

    System.out.println("subList.add(666) 獲得List :");
    System.out.println("subList : " + subList);
    System.out.println("sourceList : " + sourceList);

}
複製代碼

獲得結果:

sourceList : [H, O, L, L, I, S]
sourceList.subList(2, 5) 獲得List :
subList : [L, L, I]
subList.add(666) 獲得List :
subList : [L, L, I, 666]
sourceList : [H, O, L, L, I, 666, S]
複製代碼

咱們嘗試對subList的結構進行改變,即向其追加元素,那麼獲得的結果是sourceList的結構也一樣發生了改變。

一、結構性改變原List

public static void main(String[] args) {
    List<String> sourceList = new ArrayList<String>() {{
        add("H");
        add("O");
        add("L");
        add("L");
        add("I");
        add("S");
    }};

    List subList = sourceList.subList(2, 5);

    System.out.println("sourceList : " + sourceList);
    System.out.println("sourceList.subList(2, 5) 獲得List :");
    System.out.println("subList : " + subList);

    sourceList.add("666");

    System.out.println("sourceList.add(666) 獲得List :");
    System.out.println("sourceList : " + sourceList);
    System.out.println("subList : " + subList);

}
複製代碼

獲得結果:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1239)
    at java.util.ArrayList$SubList.listIterator(ArrayList.java:1099)
    at java.util.AbstractList.listIterator(AbstractList.java:299)
    at java.util.ArrayList$SubList.iterator(ArrayList.java:1095)
    at java.util.AbstractCollection.toString(AbstractCollection.java:454)
    at java.lang.String.valueOf(String.java:2994)
    at java.lang.StringBuilder.append(StringBuilder.java:131)
    at com.hollis.SubListTest.main(SubListTest.java:28)
複製代碼

咱們嘗試對sourceList的結構進行改變,即向其追加元素,結果發現拋出了ConcurrentModificationException。關於這個異常,咱們在《一不當心就踩坑的fail-fast是個什麼鬼?》中分析過,這裏原理相同,就再也不贅述了。

小結

咱們簡單總結一下,List的subList方法並無建立一個新的List,而是使用了原List的視圖,這個視圖使用內部類SubList表示。

因此,咱們不能把subList方法返回的List強制轉換成ArrayList等類,由於他們之間沒有繼承關係。

另外,視圖和原List的修改還須要注意幾點,尤爲是他們之間的相互影響:

一、對父(sourceList)子(subList)List作的非結構性修改(non-structural changes),都會影響到彼此。

二、對子List作結構性修改,操做一樣會反映到父List上。

三、對父List作結構性修改,會拋出異常ConcurrentModificationException。

因此,阿里巴巴Java開發手冊中有另一條規定:

如何建立新的List

若是須要對subList做出修改,又不想動原list。那麼能夠建立subList的一個拷貝:

subList = Lists.newArrayList(subList);
list.stream().skip(strart).limit(end).collect(Collectors.toList());
複製代碼

PS:最近,《阿里巴巴Java開發手冊》已經正式改名爲《Java開發手冊》,併發布了新版本,增長了21條新規約,修改描述112處。

關注公衆號後臺回覆:手冊,便可獲取最新版Java開發手冊。

參考資料: www.jianshu.com/p/585485124… www.cnblogs.com/ljdblog/p/6…

相關文章
相關標籤/搜索