Java集合細節(三):subList的缺陷

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

1、subList返回僅僅只是一個視圖

        首先咱們先看以下實例:dom

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的源碼:this

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。spa

/** 
     * 繼承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經常使用的方法。可是它的構造函數有點特殊,在該構造函數中有兩個地方須要注意:code

 

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

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

        咱們再看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返回的只是原列表的一個視圖,它全部的操做最終都會做用在原列表上

2、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):生成子列表後,不要試圖去操做原列表,不然會形成子列表的不穩定而產生異常

3、推薦使用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個建議

相關文章
相關標籤/搜索