為什麼阿里巴巴要求謹慎使用ArrayList中的subList方法
GitHub 3.7k Star 的Java工程師成神之路 ,不來了解一下嗎?
GitHub 3.7k Star 的Java工程師成神之路 ,真的不來了解一下嗎?
GitHub 3.7k Star 的Java工程師成神之路 ,真的確定不來了解一下嗎?
集合是Java開發日常開發中經常會使用到的。在之前的一些文章中,我們介紹過一些關於使用集合類應該注意的事項,如《為什麼阿里巴巴禁止在 foreach 迴圈裡進行元素的 remove/add 操作》、《為什麼阿里巴巴建議集合初始化時,指定集合容量大小》等。
關於集合類,《阿里巴巴Java開發手冊》中其實還有另外一個規定:

本文就來分析一下為什麼會有如此建議?其背後的原理是什麼?
subList
subList是List介面中定義的一個方法,該方法主要用於返回一個集合中的一段、可以理解為擷取一個集合中的部分元素,他的返回值也是一個List。
如以下程式碼:
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); }
以上程式碼輸出結果為:
[Hollis]
如果我們改動下程式碼,將subList的返回值強轉成ArrayList試一下:
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的一個檢視。
那麼,這個檢視又會帶來什麼問題呢?我們需要簡單寫幾段程式碼看一下。
1、非結構性改變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中對應的值也會發生改變。讀者可以自行嘗試一下。
1、結構性改變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的結構也同樣發生了改變。
1、結構性改變原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的修改還需要注意幾點,尤其是他們之間的相互影響:
1、對父(sourceList)子(subList)List做的非結構性修改(non-structural changes),都會影響到彼此。
2、對子List做結構性修改,操作同樣會反映到父List上。
3、對父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開發手冊。
參考資料: https://www.jianshu.com/p/5854851240df https://www.cnblogs.com/ljdblog/p/6251387.h