1. 程式人生 > >Java提高配(三七)—–Java集合細節(三):subList的缺陷

Java提高配(三七)—–Java集合細節(三):subList的缺陷

         我們經常使用subString方法來對String物件進行分割處理,同時我們也可以使用subList、subMap、subSet來對List、Map、Set進行分割處理,但是這個分割存在某些瑕疵。

一、subList返回僅僅只是一個檢視

        首先我們先看如下例項:

public static void main(String[] args) {
        List<Integer> list1 = new ArrayList<Integer>();
        list1.add(1);
        list1.add(2);
        
        //通過建構函式新建一個包含list1的列表 list2
        List<Integer> list2 = new ArrayList<Integer>(list1);
        
        //通過subList生成一個與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));
    }

        這個例子非常簡單,無非就是通過建構函式、subList重新生成一個與list1一樣的list,然後修改list3,最後比較list1 == list2?、list1 == list3?。按照我們常規的思路應該是這樣的:因為list3通過add新增了一個元素,那麼它肯定與list1不等,而list2是通過list1構造出來的,所以應該相等,所以結果應該是:

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

        首先我們先不論結果的正確與否,我們先看subList的原始碼:

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

subListRangeCheck方式是判斷fromIndex、toIndex是否合法,如果合法就直接返回一個subList物件,注意在產生該new該物件的時候傳遞了一個引數 this ,該引數非常重要,因為他代表著原始list。

/**
     * 繼承AbstractList類,實現RandomAccess介面
     */
    private class SubList extends AbstractList<E> implements RandomAccess {
        private final AbstractList<E> parent;    //列表
        private final int parentOffset;   
        private final int offset;
        int size;

        //建構函式
        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的內部類,它與ArrayList一樣,都是繼承AbstractList和實現RandomAccess介面。同時也提供了get、set、add、remove等list常用的方法。但是它的建構函式有點特殊,在該建構函式中有兩個地方需要注意:

        1、this.parent = parent;而parent就是在前面傳遞過來的list,也就是說this.parent就是原始list的引用。

        2、this.offset = offset + fromIndex;this.parentOffset = fromIndex;。同時在建構函式中它甚至將modCount(fail-fast機制)傳遞過來了。

        我們再看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;

誠然,到了這裡我們可以判斷subList返回的SubList同樣也是AbstractList的子類,同時它的方法如get、set、add、remove等都是在原列表上面做操作,它並沒有像subString一樣生成一個新的物件。所以subList返回的只是原列表的一個檢視,它所有的操作最終都會作用在原列表上。

那麼從這裡的分析我們可以得出上面的結果應該恰恰與我們上面的答案相反:

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

Java細節(3.1):subList返回的只是原列表的一個檢視,它所有的操作最終都會作用在原列表上

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

        從上面我們知道subList生成的子列表只是原列表的一個檢視而已,如果我們操作子列表它產生的作用都會在原列表上面表現,但是如果我們操作原列表會產生什麼情況呢?

public static void main(String[] args) {
        List<Integer> list1 = new ArrayList<Integer>();
        list1.add(1);
        list1.add(2);
        
        //通過subList生成一個與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());
    }

該例項如果不產生意外,那麼他們兩個list的大小都應該都是3,但是偏偏事與願違,事實上我們得到的結果是這樣的:

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異常,看過我另一篇部落格的同仁肯定對這個異常非常,fail-fast?不錯就是fail-fast機制,在fail-fast機制中,LZ花了很多力氣來講述這個異常,所以這裡LZ就不對這個異常多講了(更多請點這裡:Java提高篇(三四)—–fail-fast機制)。我們再看size方法:

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

size方法首先會通過checkForComodification驗證,然後再返回this.size。

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

該方法表明當原列表的modCount與this.modCount不相等時就會丟擲ConcurrentModificationException。同時我們知道modCount 在new的過程中 “繼承”了原列表modCount,只有在修改該列表(子列表)時才會修改該值(先表現在原列表後作用於子列表)。而在該例項中我們是操作原列表,原列表的modCount當然不會反應在子列表的modCount上啦,所以才會丟擲該異常。

對於子列表檢視,它是動態生成的,生成之後就不要操作原列表了,否則必然都導致檢視的不穩定而丟擲異常。最好的辦法就是將原列表設定為只讀狀態,要操作就操作子列表:

//通過subList生成一個與list1一樣的列表 list3
List<Integer> list3 = list1.subList(0, list1.size());
        
//對list1設定為只讀狀態
list1 = Collections.unmodifiableList(list1);

Java細節(3.2):生成子列表後,不要試圖去操作原列表,否則會造成子列表的不穩定而產生異常

三、推薦使用subList處理區域性列表

在開發過程中我們一定會遇到這樣一個問題:獲取一堆資料後,需要刪除某段資料。例如,有一個列表存在1000條記錄,我們需要刪除100-200位置處的資料,可能我們會這樣處理:

for(int i = 0 ; i < list1.size() ; i++){
   if(i >= 100 && i <= 200){
       list1.remove(i);
       /*
        * 當然這段程式碼存在問題,list remove之後後面的元素會填充上來,
         * 所以需要對i進行簡單的處理,當然這個不是這裡討論的問題。
         */
   }
}

這個應該是我們大部分人的處理方式吧,其實還有更好的方法,利用subList。在前面LZ已經講過,子列表的操作都會反映在原列表上。所以下面一行程式碼全部搞定:

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

簡單而不失華麗!!!!!

參考資料:編寫高質量程式碼:改善Java程式的151個建議