【Java基礎提升】List集合使用細節

是List裏面還有很多不爲人知的坑,下面就來總結下常見的一些坑

一.Arrays.asList()

  1. 坑1:使用Arrays.asList轉換後的對象進行add/remove
    @Test
    public void asListToAddRemove() {
        String[] arrays = {"1", "2", "3"};
        List<String> list = Arrays.asList(arrays);
        list.add("4");
        //java.utilArrays$ArrayList (ArrayList是java.util.Arrays的內部類)
    }

Arrays.asList將一個數組轉化爲List然後再添加一個元素,會拋出如下異常:
在這裏插入圖片描述
原因: 通過源碼可以看出 Arrays.aslist得到的不是java.util.ArrayList而是一個Arrays一個內部類java.utilArrays$ArrayList
在這裏插入圖片描述
Debug結果:
在這裏插入圖片描述

​ addremove方法實際都來自父類AbstractListjava.util.Arrays$ArrayList並沒有重寫父類的方法,而會拋出UnsupportedOperationException。這也是爲什麼不支持增刪的原因。

  1. 坑2:使用Arrays.asList轉換後的對象和原對象進行操作
    @Test
    public void asListReference() {
        String[] arrays = {"1", "2", "3"};
        List<String> list = Arrays.asList(arrays);

        list.set(0, "a");
        System.out.println("arrays=" + JSONObject.toJSONString(arrays) + ",list=" + JSONObject.toJSONString(list));
        //arrays=["a","2","3"],list=["a","2","3"]
        arrays[2] = "c";
        System.out.println("arrays=" + JSONObject.toJSONString(arrays) + ",list=" + JSONObject.toJSONString(list));
        ////arrays=["a","2","c"],list=["a","2","c"]
        //可以看到,不管是修改原數組還是新的list集合兩者都會互相影響。因爲這個方法實現的時候使用了原始的數組

    }

發現: Arrays.asList轉換後的對象,不管是修改原數組還是新的list集合兩者都會互相影響。
在這裏插入圖片描述
原因: java.util.Arrays$ArrayList內部類僅僅保存的是原數組的內存地址
在這裏插入圖片描述
解決辦法:

        String[] arrays = {"1", "2", "3"};
        List<String> list = Arrays.asList(arrays);

        list.set(0, "a");
        arrays[2] = "c";

//解決辦法1: 使用java.util.ArrayList存放Arrays.asList轉換後的對象
        List<String> newList = new ArrayList<>(Arrays.asList(arrays));
        newList.add("newList");
        System.out.println("arrays=" + JSONObject.toJSONString(arrays) + ",list=" + JSONObject.toJSONString(list) + ",newList=" + JSONObject.toJSONString(newList));
        //arrays=["a","2","c"],list=["a","2","c"],newList=["a","2","c","new"]

//解決辦法2:谷歌提供的Guava  Lists.newArrayList方法
        List<String> newList2 = Lists.newArrayList(newList);
        newList2.add("newList2");
        System.out.println("arrays=" + JSONObject.toJSONString(arrays) + ",list=" + JSONObject.toJSONString(list) + ",newList1=" + JSONObject.toJSONString(newList) + ",newList2=" + JSONObject.toJSONString(newList2));
        //arrays=["a","2","c"],list=["a","2","c"],newList1=["a","2","c","newList"],newList2=["a","2","c","newList","newList2"]

二.foreach循環刪除元素

foreach 增加/刪除元素大坑

使用foreach刪除值爲1的元素

    @Test
    public void listForeachAddAndRemoveException() {
        String[] arrays = {"1", "2", "3"};
        List<String> list = new ArrayList<String>(Arrays.asList(arrays));
        for (String str : list) {
            if (str.equals("1")) {
                list.remove(str);
            }
        }
    }

執行結果:
在這裏插入圖片描述
原因:​ 可以看到最終錯誤是在java.util.ArrayList$Itr.next處拋出,但我們並沒有調用該方法,實際上foreach這種寫法是一種語法糖,其底層還是使用Iterator迭代器實現方法。

反編譯結果
在這裏插入圖片描述
解決辦法:

  1. 使用Iteratorremove方法刪除元素
    @Test
    public void listForeachAddAndRemoveExceptionByIterator() {
        String[] arrays = {"1", "2", "3"};
        List<String> list = new ArrayList<String>(Arrays.asList(arrays));
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            String str = it.next();
            if (str.equals("1")) {
                it.remove();
            }
        }

        System.out.println(list);//[2, 3]
    }
  1. 使用Java8removeIf方法
    @Test
    public void listForeachAddAndRemoveExceptionByRemoveIf() {
        String[] arrays = {"1", "2", "3"};
        List<String> list = new ArrayList<String>(Arrays.asList(arrays));
        list.removeIf(str -> str.equals("1"));
        System.out.println(list);//[2, 3]
    }

removeIf的底層還是使用Iterator
在這裏插入圖片描述

三. ArrayList.subList()

1.舉例說明

使用subList()、subMap()、subSet() 可以 List、Map、Set 進行分割處理,但這些方法都存在一些坑,下面我們以subList爲例進行講解:

subList: 是List接口中定義的一個方法,該方法主要用於返回一個集合中的一段、可以理解爲截取一個集合中的部分元素,他的返回值也是一個List。

    @Test
    public void testSubList() {
        List<Integer> oriList = new ArrayList<>();
        oriList.add(1);
        oriList.add(2);

        // 通過構造函數新建一個包含oriList 的集合 arrayList 
        List<Integer> arrayList = new ArrayList<>(oriList);

        // 通過subList方法生成一個與oriList一樣的集合 subList
        List<Integer> subList = oriList.subList(0, oriList.size());

        //操作截取後生成的子集合
        subList.add(3);


        System.out.println("oriList == arrayList:" + oriList.equals(arrayList) + "----oriList=" + oriList + ",arrayList=" + arrayList);
        //oriList == arrayList:false----oriList=[1, 2, 3],arrayList=[1, 2]
        System.out.println("oriList == subList:" + oriList.equals(subList) + "----oriList=" + oriList + ",subList=" + subList);//true
        //oriList == subList:true----oriList=[1, 2, 3],subList=[1, 2, 3]
    }

上面是通過構造函數包含原始集合或者截取原始集合重新生成一個原始集合一樣的list,然後修改截取後的集合subList,最後比較oriList==arrayList、oriList== subList

按照我們常規的思路應該是這樣的:

  • arrayList是通過oriList構造出來的,所以應該相等
  • subList通過add新增了一個元素,那麼它肯定與oriList不等

理論是應該是:

	oriList == arrayList:true
	oriList == subList:false

但真實結果是:

	oriList == arrayList:false
	oriList == subList: true

2.源碼分析

我們先不論結果的正確與否,看看subList方法的源碼:

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

subListRangeCheck() 方法是判斷 fromIndextoIndex 是否合法,如果合法就直接返回一個 subList 對象。這裏需要注意2點:

  • 在產生該 new 該對象的時候傳遞了一個參數 this ,該參數非常重要,因爲他代表着原始 list
  • SubList類是ArrayList的一個內部類,這個類很特別。
    static void subListRangeCheck(int fromIndex, int toIndex, int size) {
        if (fromIndex < 0)
            throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
        if (toIndex > size)
            throw new IndexOutOfBoundsException("toIndex = " + toIndex);
        if (fromIndex > toIndex)
            throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                               ") > toIndex(" + toIndex + ")");
    }
	/**
     * 繼承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;
 
        //構造函數的parent參數很重要,傳的是當前對象的引用
        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;
        }
        //-----------------------省略其他方法----------------
    }

第一個特別點是它的構造函數,在該構造函數中有2個地方需要注意:

  • this.parent = parent;這裏的parent 就是在前面傳遞過來的 list,也就是說 this.parent 就是原始 list 的引用
  • this.offset = offset + fromIndex;this.parentOffset = fromIndex;。同時在構造函數中它將 modCount(fail-fast機制)也傳遞過來了。

第二個特別點是它的普通方法

  • get 方法
    return ArrayList.this.elementData(offset + index);這段代碼可以清晰表明 get 所返回就是原集合offset + index位置的元素。
        public E get(int index) {
            rangeCheck(index);
            checkForComodification();
            return ArrayList.this.elementData(offset + index);
        }
  • add 方法
    parent.add(parentOffset + index, e); this.modCount = parent.modCount; 表明是在原集合上添加 原集合長度parentOffset +當前下標index位置上添加元素
public void add(int index, E e) {
    rangeCheckForAdd(index);
    checkForComodification();
    parent.add(parentOffset + index, e); // 注意這裏
    this.modCount = parent.modCount;
    this.size++;
}
  • remove方法:
    parent.remove(parentOffset + index); this.modCount = parent.modCount; 表明刪除原集合 原集合長度parentOffset +當前下標index位置的元素
public E remove(int index) {
    rangeCheck(index);
    checkForComodification();
    E result = parent.remove(parentOffset + index); // 注意這裏
    this.modCount = parent.modCount;
    this.size--;
    return result;
}

由以上源碼,可以判斷 subList() 方法返回的 SubList 同樣也是 AbstractList 的子類,同時它的方法如 get、set、add、remove 等都是在原list上面做操作,而不是生成一個新的對象

結論:

subList返回的只是原列表的一個視圖,它所有的操作最終都會作用在原列表上

視圖理解

視圖就是集合或者映射中某一部分或者某一類數據的再映射得到的結果集,這個結果集一般不允許更新(有些視圖允許更新某個元素,但是不允許新增或者刪除元素),只允許讀取,結果集中的數據使用的還是原來集合或者映射中的數據的引用。
.
個人理解: 集合視圖就是把集合裏面的東西給你展示出來度, 僅供查看(有的允許更新,但不允許刪除).。 Colletion視圖,Map視圖都是將集合裏面的內容比如: Map.keySet(), 得到Map的鍵值視圖, Map.values()得到Map的值視圖, Map.entrySet()得到鍵/值對視圖

3.操作調用subList()方法的原始集合

從上面我們知道

  • subList截取生成的子集合只是原集合的一個視圖而已,如果我們操作子集合它產生的作用都會在原集合上面表現,但是如果我們操作原集合會產生什麼情況呢?
    @Test
    public void handleOriList() {
        List<Integer> oriList = new ArrayList<Integer>();
        oriList.add(1);
        oriList.add(2);

        //通過subList生成一個與oriList一樣的列表 subList
        List<Integer> subList = oriList.subList(0, oriList.size());

        //修改原集合
        oriList.add(3);

        System.out.println("oriList.size=" + oriList.size());
        System.out.println("subList.size-" + subList.size());
    }

執行結果:
在這裏插入圖片描述

  • oriList正常輸出,但是subList就拋出ConcurrentModificationException異常(原因爲fast-fail機制

  • 通過觀察源碼,size方法首先會通過checkForComodification驗證,然後再返回this.size

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

private void checkForComodification() {
     if (ArrayList.this.modCount != this.modCount)
     throw new ConcurrentModificationException();
}
  • checkForComodification方法 表明當原集合的modCountthis.modCount不相等時就會拋出ConcurrentModificationException
  • 同時modCountnew的過程中 “繼承”了原集合modCount,只有在修改該集合(子集合)時纔會修改該值(先表現在原集合後作用於子集合)。而在該實例中我們是操作原集合,原集合的modCount當然不會反應在子集合的modCount上啦,所以纔會拋出該異常。
  • 對於子集合,它是動態生成的,生成之後就不要操作原集合了,否則必定會導致子集合的不穩定而拋出異常。最好的辦法就是將原集合設置爲只讀狀態,要操作就操作子列表
//通過subList()方法生成一個與oriList一樣的列表 subList
List<Integer> subList= oriList.subList(0, oriList.size());
        
//對oriList設置爲只讀狀態
oriList= Collections.unmodifiableList(oriList);

結論:

subList()生成子集合後,不要試圖去操作原集合,否則會造成子集合的不穩定而產生異常

4.刪除某一段集合

在開發中獲取一堆數據後,需要刪除某段數據。如,有一個列表存在1000條記錄,我們需要刪除100-200位置處的數據,可能我們會這樣處理:

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

上面代碼原因: List每remove掉一個元素以後,後面的元素都會向前移動,此時如果執行i=i+1,則剛剛移過來的元素沒有被讀取。

解決辦法:

  • 利用子列表的操作都會反映在原列表上原理, 使用subList()截取指定返回集合後調用clear()清空子集合
oriList.subList(100, 200).clear();

5.如何創建新的List

利用Java8的stream特性

list.stream().skip(strart).limit(end).collect(Collectors.toList());

6.小結

我們簡單總結一下,List的subList方法並沒有創建一個新的List,而是使用了原List的視圖,這個視圖使用內部類SubList表示。
所以,我們不能把subList方法返回的List強制轉換成ArrayList等類,因爲他們之間沒有繼承關係。

另外,視圖和原List的修改還需要注意幾點,尤其是他們之間的相互影響:

  1. 對父(sourceList)子(subList)List做的非結構性修改(non-structural changes),都會影響到彼此。
  2. 對子List做結構性修改,操作同樣會反映到父List上。
  3. 對父List做結構性修改,會拋出異常ConcurrentModificationException。
    所以,阿里巴巴Java開發手冊中有另外一條規定:在這裏插入圖片描述

m/p/73722487)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章