鍍金池/ 教程/ Java/ Java 集合細(xì)節(jié)(三):subList 的缺陷
Java 集合細(xì)節(jié)(四):保持 compareTo 和 equals 同步
Iterator
使用序列化實(shí)現(xiàn)對(duì)象的拷貝
fail-fast 機(jī)制
關(guān)鍵字 final
Vector
HashTable
Java 集合細(xì)節(jié)(一):請(qǐng)為集合指定初始容量
強(qiáng)制類(lèi)型轉(zhuǎn)換
數(shù)組之一:認(rèn)識(shí) JAVA 數(shù)組
Java 集合細(xì)節(jié)(三):subList 的缺陷
hashCode
ArrayList
數(shù)組之二
List 總結(jié)
LinkedList
Java 提高篇(九)—–實(shí)現(xiàn)多重繼承
Java 的四舍五入
關(guān)鍵字 static
理解 Java 的三大特性之多態(tài)
抽象類(lèi)與接口
集合大家族
異常(二)
Java 集合細(xì)節(jié)(二):asList 的缺陷
Map 總結(jié)
TreeSet
equals() 方法總結(jié)
Java 提高篇(十)—–詳解匿名內(nèi)部類(lèi)
HashMap
Stack
詳解內(nèi)部類(lèi)
TreeMap
異常(一)
詳解 Java 定時(shí)任務(wù)
HashSet
字符串
理解 Java 的三大特性之繼承
理解 Java 的三大特性之封裝
代碼塊

Java 集合細(xì)節(jié)(三):subList 的缺陷

我們經(jīng)常使用 subString 方法來(lái)對(duì) String 對(duì)象進(jìn)行分割處理,同時(shí)我們也可以使用 subList、subMap、subSet 來(lái)對(duì) List、Map、Set 進(jìn)行分割處理,但是這個(gè)分割存在某些瑕疵。

一、subList 返回僅僅只是一個(gè)視圖

首先我們先看如下實(shí)例:


    public static void main(String[] args) {
            List<Integer> list1 = new ArrayList<Integer> ();
            list1.add(1);
            list1.add(2);

            //通過(guò)構(gòu)造函數(shù)新建一個(gè)包含list1的列表 list2
            List<Integer> list2 = new ArrayList<Integer>(list1);

            //通過(guò)subList生成一個(gè)與list1一樣的列表 list3
            List<Integer> list3 = list1.subList(0, list1.size());

            //修改list3
            list3.add(3);

            System.out.println("list1 == list2:" + list1.equals(list2));
            System.out.println("list1 == list3:" + list1.equals(list3));
        }

這個(gè)例子非常簡(jiǎn)單,無(wú)非就是通過(guò)構(gòu)造函數(shù)、subList 重新生成一個(gè)與 list1 一樣的 list,然后修改 list3,最后比較 list1 == list2?、list1 == list3?。按照我們常規(guī)的思路應(yīng)該是這樣的:因?yàn)?list3 通過(guò) add 新增了一個(gè)元素,那么它肯定與 list1 不等,而 list2 是通過(guò) list1 構(gòu)造出來(lái)的,所以應(yīng)該相等,所以結(jié)果應(yīng)該是:


    list1 == list2:true
    list1 == list3: false

首先我們先不論結(jié)果的正確與否,我們先看 subList 的源碼:


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

subListRangeCheck 方式是判斷 fromIndex、toIndex 是否合法,如果合法就直接返回一個(gè) subList 對(duì)象,注意在產(chǎn)生該 new 該對(duì)象的時(shí)候傳遞了一個(gè)參數(shù) this ,該參數(shù)非常重要,因?yàn)樗碇?list。


    /**
         * 繼承AbstractList類(lèi),實(shí)現(xiàn)RandomAccess接口
         */
        private class SubList extends AbstractList<E> implements RandomAccess {
            private final AbstractList<E> parent;    //列表
            private final int parentOffset;   
            private final int offset;
            int size;

            //構(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;
            }

            //set方法
            public E set(int index, E e) {
                rangeCheck(index);
                checkForComodification();
                E oldValue = ArrayList.this.elementData(offset + index);
                ArrayList.this.elementData[offset + index] = e;
                return oldValue;
            }

            //get方法
            public E get(int index) {
                rangeCheck(index);
                checkForComodification();
                return ArrayList.this.elementData(offset + index);
            }

            //add方法
            public void add(int index, E e) {
                rangeCheckForAdd(index);
                checkForComodification();
                parent.add(parentOffset + index, e);
                this.modCount = parent.modCount;
                this.size++;
            }

            //remove方法
            public E remove(int index) {
                rangeCheck(index);
                checkForComodification();
                E result = parent.remove(parentOffset + index);
                this.modCount = parent.modCount;
                this.size--;
                return result;
            }
        }

該 SubLsit 是 ArrayList 的內(nèi)部類(lèi),它與 ArrayList 一樣,都是繼承 AbstractList 和實(shí)現(xiàn) RandomAccess 接口。同時(shí)也提供了 get、set、add、remove 等 list 常用的方法。但是它的構(gòu)造函數(shù)有點(diǎn)特殊,在該構(gòu)造函數(shù)中有兩個(gè)地方需要注意:

1、this.parent = parent;而 parent 就是在前面?zhèn)鬟f過(guò)來(lái)的 list,也就是說(shuō) this.parent 就是原始 list 的引用。

2、this.offset = offset + fromIndex;this.parentOffset = fromIndex;。同時(shí)在構(gòu)造函數(shù)中它甚至將 modCount(fail-fast機(jī)制)傳遞過(guò)來(lái)了。

我們?cè)倏?get 方法,在 get 方法中 return ArrayList.this.elementData(offset + index);這段代碼可以清晰表明 get 所返回就是原列表 offset + index位置的元素。同樣的道理還有 add 方法里面的:


    parent.add(parentOffset + index, e);
    this.modCount = parent.modCount;

remove 方法里面的


    E result = parent.remove(parentOffset + index);
    this.modCount = parent.modCount;

誠(chéng)然,到了這里我們可以判斷 subList 返回的 SubList 同樣也是 AbstractList 的子類(lèi),同時(shí)它的方法如 get、set、add、remove 等都是在原列表上面做操作,它并沒(méi)有像 subString 一樣生成一個(gè)新的對(duì)象。所以 subList 返回的只是原列表的一個(gè)視圖,它所有的操作最終都會(huì)作用在原列表上。

那么從這里的分析我們可以得出上面的結(jié)果應(yīng)該恰恰與我們上面的答案相反:


    list1 == list2:false
    list1 == list3:true

Java 細(xì)節(jié)(3.1):subList 返回的只是原列表的一個(gè)視圖,它所有的操作最終都會(huì)作用在原列表上

二、subList 生成子列表后,不要試圖去操作原列表

從上面我們知道 subList 生成的子列表只是原列表的一個(gè)視圖而已,如果我們操作子列表它產(chǎn)生的作用都會(huì)在原列表上面表現(xiàn),但是如果我們操作原列表會(huì)產(chǎn)生什么情況呢?


    public static void main(String[] args) {
            List<Integer> list1 = new ArrayList<Integer>();
            list1.add(1);
            list1.add(2);

            //通過(guò)subList生成一個(gè)與list1一樣的列表 list3
            List<Integer> list3 = list1.subList(0, list1.size());
            //修改list3
            list1.add(3);

            System.out.println("list1'size:" + list1.size());
            System.out.println("list3'size:" + list3.size  ());
        }

該實(shí)例如果不產(chǎn)生意外,那么他們兩個(gè) list 的大小都應(yīng)該都是 3,但是偏偏事與愿違,事實(shí)上我們得到的結(jié)果是這樣的:


    list1'size:3
    Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.ArrayList$SubList.checkForComodification(Unknown Source)
        at java.util.ArrayList$SubList.size(Unknown Source)
        at com.chenssy.test.arrayList.SubListTest.main(SubListTest.java:17)

list1 正常輸出,但是 list3 就拋出 ConcurrentModificationException 異常,看過(guò)我另一篇博客的同仁肯定對(duì)這個(gè)異常非常,fail-fast?不錯(cuò)就是 fail-fast 機(jī)制,在 fail-fast 機(jī)制中,LZ 花了很多力氣來(lái)講述這個(gè)異常,所以這里 LZ 就不對(duì)這個(gè)異常多講了(更多請(qǐng)點(diǎn)這里:Java 提高篇(三四)—–fail-fast 機(jī)制 )。我們?cè)倏?size 方法:


    public int size() {
                checkForComodification();
                return this.size;
            }

size 方法首先會(huì)通過(guò) checkForComodification 驗(yàn)證,然后再返回this.size。


    private void checkForComodification() {
                if (ArrayList.this.modCount != this.modCount)
                    throw new ConcurrentModificationException();
            }

該方法表明當(dāng)原列表的 modCount 與 this.modCount 不相等時(shí)就會(huì)拋出 ConcurrentModificationException。同時(shí)我們知道 modCount 在 new 的過(guò)程中 “繼承”了原列表 modCount,只有在修改該列表(子列表)時(shí)才會(huì)修改該值(先表現(xiàn)在原列表后作用于子列表)。而在該實(shí)例中我們是操作原列表,原列表的 modCount 當(dāng)然不會(huì)反應(yīng)在子列表的 modCount 上啦,所以才會(huì)拋出該異常。

對(duì)于子列表視圖,它是動(dòng)態(tài)生成的,生成之后就不要操作原列表了,否則必然都導(dǎo)致視圖的不穩(wěn)定而拋出異常。最好的辦法就是將原列表設(shè)置為只讀狀態(tài),要操作就操作子列表:


    //通過(guò)subList生成一個(gè)與list1一樣的列表 list3
    List<Integer> list3 = list1.subList(0, list1.size());

    //對(duì)list1設(shè)置為只讀狀態(tài)
    list1 = Collections.unmodifiableList(list1);

Java 細(xì)節(jié)(3.2):生成子列表后,不要試圖去操作原列表,否則會(huì)造成子列表的不穩(wěn)定而產(chǎn)生異常

三、推薦使用 subList 處理局部列表

在開(kāi)發(fā)過(guò)程中我們一定會(huì)遇到這樣一個(gè)問(wèn)題:獲取一堆數(shù)據(jù)后,需要?jiǎng)h除某段數(shù)據(jù)。例如,有一個(gè)列表存在 1000 條記錄,我們需要?jiǎng)h除 100-200 位置處的數(shù)據(jù),可能我們會(huì)這樣處理:


    for(int i = 0 ; i < list1.size() ; i++){
       if(i >= 100 && i <= 200){
           list1.remove(i);
           /*
            * 當(dāng)然這段代碼存在問(wèn)題,list remove之后后面的元素會(huì)填充上來(lái),
            * 所以需要對(duì)i進(jìn)行簡(jiǎn)單的處理,當(dāng)然這個(gè)不是這里討論的 問(wèn)題。
            */
       }
    }

這個(gè)應(yīng)該是我們大部分人的處理方式吧,其實(shí)還有更好的方法,利用 subList。在前面 LZ 已經(jīng)講過(guò),子列表的操作都會(huì)反映在原列表上。所以下面一行代碼全部搞定:


    list1.subList(100, 200).clear();

簡(jiǎn)單而不失華麗?。。。?!

參考資料:編寫(xiě)高質(zhì)量代碼:改善 Java 程序的 151 個(gè)建議

上一篇:Stack下一篇:Java 的四舍五入