為什么阿里巴巴要求謹(jǐn)慎使用ArrayList中的subList方法
集合是Java開發(fā)日常開發(fā)中經(jīng)常會使用到的。在之前的一些文章中,我們介紹過一些關(guān)于使用集合類應(yīng)該注意的事項,如《為什么阿里巴巴禁止在 foreach 循環(huán)里進(jìn)行元素的 remove/add 操作》、《為什么阿里巴巴建議集合初始化時,指定集合容量大小》等。
關(guān)于集合類,《阿里巴巴Java開發(fā)手冊》中其實還有另外一個規(guī)定:
本文就來分析一下為什么會有如此建議?其背后的原理是什么?
1.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);
- }
以上代碼輸出結(jié)果為:
- [Hollis]
如果我們改動下代碼,將subList的返回值強(qiáng)轉(zhuǎn)成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
不只是強(qiáng)轉(zhuǎn)成ArrayList會報錯,強(qiáng)轉(zhuǎn)成LinkedList、Vector等List的實現(xiàn)類同樣也都會報錯。
那么,為什么會發(fā)生這樣的報錯呢?我們接下來深入分析一下。
2.底層原理
首先,我們看下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中的一個內(nèi)部類。
SubList這個類中單獨(dú)定義了set、get、size、add、remove等方法。
當(dāng)我們調(diào)用subList方法的時候,會通過調(diào)用SubList的構(gòu)造函數(shù)創(chuàng)建一個SubList,那么看下這個構(gòu)造函數(shù)做了哪些事情:
- 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;
- }
可以看到,這個構(gòu)造函數(shù)中把原來的List以及該List中的部分屬性直接賦值給自己的一些屬性了。
也就是說,SubList并沒有重新創(chuàng)建一個List,而是直接引用了原有的List(返回了父類的視圖),只是指定了一下他要使用的元素的范圍而已(從fromIndex(包含),到toIndex(不包含))。
所以,為什么不能講subList方法得到的集合直接轉(zhuǎn)換成ArrayList呢?因為SubList只是ArrayList的內(nèi)部類,他們之間并沒有繼承關(guān)系,故無法直接進(jìn)行強(qiáng)制類型轉(zhuǎn)換。
3.視圖有什么問題
前面通過查看源碼,我們知道,subList()方法并沒有重新創(chuàng)建一個ArrayList,而是返回了一個ArrayList的內(nèi)部類——SubList。
這個SubList是ArrayList的一個視圖。
那么,這個視圖又會帶來什么問題呢?我們需要簡單寫幾段代碼看一下。
1、非結(jié)構(gòu)性改變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);
- }
得到結(jié)果:
- 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]
當(dāng)我們嘗試通過set方法,改變subList中某個元素的值得時候,我們發(fā)現(xiàn),原來的那個List中對應(yīng)元素的值也發(fā)生了改變。
同理,如果我們使用同樣的方法,對sourceList中的某個元素進(jìn)行修改,那么subList中對應(yīng)的值也會發(fā)生改變。讀者可以自行嘗試一下。
2、結(jié)構(gòu)性改變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);
- }
得到結(jié)果:
- 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的結(jié)構(gòu)進(jìn)行改變,即向其追加元素,那么得到的結(jié)果是sourceList的結(jié)構(gòu)也同樣發(fā)生了改變。
3、結(jié)構(gòu)性改變原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);
- }
得到結(jié)果:
- 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的結(jié)構(gòu)進(jìn)行改變,即向其追加元素,結(jié)果發(fā)現(xiàn)拋出了ConcurrentModificationException。
【本文是51CTO專欄作者Hollis的原創(chuàng)文章,作者微信公眾號Hollis(ID:hollischuang)】