天天看點

List操作的一些常見問題

作者:Java碼農之路
List操作的一些常見問題

阿裡巴巴開發手冊強制規約:

List操作的一些常見問題

1. Arrays.asList轉換基本類型數組

在實際的業務開發中,我們通常會進行數組轉List的操作,通常我們會使用Arrays.asList來進行轉換,但是在轉換基本類型的數組的時候,卻出現轉換的結果和我們想象的不一緻。

觀察下asList的實作,可以看到是入參是使用的是泛型<T>,是以會将{1, 2, 3}三個整數放入一個泛型清單中傳回。

import java.util.Arrays;
import java.util.List;

/**
 * Arrays.asList數組常見問題
 * @author 百裡
 */
public class BaiLiTestDemo {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        List list = Arrays.asList(arr);
        System.out.println("list.size:" + list.size());
        for (int i = 0; i < list.size(); i++) {
            System.out.println("循環列印:" + list.get(i));
        }
    }
}           

觀察下asList的實作,可以看到是入參是使用的是泛型<T>,是以會将{1, 2, 3}三個整數放入一個泛型清單中傳回。

public static List asList(T... a) {
    return new ArrayList<>(a); 
}           
List操作的一些常見問題

那我們該如何解決呢?隻需要在聲明數組的時候,聲明類型改為包裝類型。

import java.util.Arrays;
import java.util.List;

/**
 * Arrays.asList數組常見問題
 * @author 百裡
 */
public class BaiLiTestDemo {
    public static void main(String[] args) {
        Integer[] arr = {1, 2, 3};
        List list = Arrays.asList(arr);
        System.out.println("list.size:" + list.size());//size = 3
        for (int i = 0; i < list.size(); i++) {
            System.out.println("循環列印:" + list.get(i));
        }
    }
}           

這就是第一個坑了,然而Arrays.asList不止這一個需要注意的問題,我們繼續往下看:

2. Arrays.asList傳回的List不支援增删操作

我們接着上面的demo,增加list加減的邏輯,運作demo會提示UnsupportedOperationException:

import java.util.Arrays;
import java.util.List;

/**
 * Arrays.asList數組常見問題
 * @author 百裡
 */
public class BaiLiTestDemo {
    public static void main(String[] args) {
        Integer[] arr = {1, 2, 3};
        List list = Arrays.asList(arr);
        System.out.println("list.size:" + list.size());
        list.add(4);
    }
}           

為什麼會這樣?我們看下asList的實作,它傳回的ArrayList是Arrays的内部類,而不是我們通常使用的java.util.ArrayList:

List操作的一些常見問題

可以看到内部類中的ArrayList沒有add()與remove(),那我們怎麼可以使用增減方法呢,繼續往下看:

List操作的一些常見問題

可以看到ArrayList繼承了AbstractList類,我們觀察AbstractList類的add()與remove():

List操作的一些常見問題

現在是不是就了解Arrays.asList傳回的List不支援增删操作了。

3. 對原始數組的修改會影響到我們獲得的那個List

基于第一個demo我們繼續改造,修改原arr[0]=10,這個時候列印Arrays.asList傳回的list值也發生了改變:

import java.util.Arrays;
import java.util.List;

/**
 * Arrays.asList數組常見問題
 * @author 百裡
 */
public class BaiLiTestDemo {
    public static void main(String[] args) {
        Integer[] arr = {1, 2, 3};
        List list = Arrays.asList(arr);
        System.out.println("list.size:" + list.size());
        arr[0] = 10;//修改原數組
        for (int i = 0; i < list.size(); i++) {
            System.out.println("循環列印:" + list.get(i));
        }
    }
}           

為什麼呢?觀察ArrayList的實作,可以知道asList建立了 ArrayList,但它直接引用原本的資料組對象。是以隻要原本的數組對象一發生變化,List也跟着變化。

List操作的一些常見問題

解決方案:new一個新的ArrayList裝Arrays.asList傳回資料。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Arrays.asList數組常見問題
 * @author 百裡
 */
public class BaiLiTestDemo {
    public static void main(String[] args) {
        Integer[] arr = {1, 2, 3};
        List list = new ArrayList<>(Arrays.asList(arr));
        arr[0] = 10;
        for (int i = 0; i < list.size(); i++) {
            System.out.println("循環列印:" + list.get(i));
        }
    }
}           

4. ArrayList.subList強轉ArrayList導緻異常

當使用ArrayList.subList的傳回list強轉ArrayList時,會出現java.lang.ClassCastException,看以下代碼:

import java.util.ArrayList;
import java.util.List;

/**
 * ArrayList.subList常見問題
 * @author 百裡
 */
public class BaiLiArrayListDemo {
    public static void main(String[] args) {
        List<String> names = new ArrayList<String>() {{
            add("one");
            add("two");
            add("three");
        }};
        ArrayList strings = (ArrayList) names.subList(0, 1);
        System.out.println(strings);
    }
}           
Exception in thread "main" java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList
	at BaiLiArrayListDemo.main(BaiLiArrayListDemo.java:15)           

同樣的,我們看下sublist的實作:

List操作的一些常見問題

可以看到SubList()實際上沒有建立一個新的List,而是直接引用了原來的List,指定了元素的範圍。并且傳回的是一個内部類實作的SubList對象,該對象隻是原始ArrayList的一個引用,而不是一個全新的ArrayList,是以無法直接将其強制轉換為ArrayList類型。

由于是引用的原List,是以也會存在asList的問題,也就是針對subList進行增減資料,會影響原List的值。

import java.util.ArrayList;
import java.util.List;

/**
 * ArrayList.subList常見問題
 * @author 百裡
 */
public class BaiLiArrayListDemo {
    public static void main(String[] args) {
        List<String> names = new ArrayList<String>() {{
            add("one");
            add("two");
            add("three");
        }};
        List  strings = names.subList(0, 1);
        strings.add(0,"four");
        System.out.println(strings);//[four, one]
        System.out.println(names);//[four, one, two, three]
    }
}           

需要注意修改原List-names的值會出導緻strings的周遊、增加、删除産生ConcurrentModificationException異常。

import java.util.ArrayList;
import java.util.List;

/**
 * ArrayList.subList常見問題
 * @author 百裡
 */
public class BaiLiArrayListDemo {
    public static void main(String[] args) {
        List<String> names = new ArrayList<String>() {{
            add("one");
            add("two");
            add("three");
        }};
        List strings = names.subList(0, 1);
        names.add("four");
        System.out.println(strings);
        System.out.println(names);
    }
}           
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1231)
	at java.util.ArrayList$SubList.listIterator(ArrayList.java:1091)
	at java.util.AbstractList.listIterator(AbstractList.java:299)
	at java.util.ArrayList$SubList.iterator(ArrayList.java:1087)
	at java.util.AbstractCollection.toString(AbstractCollection.java:454)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at BaiLiArrayListDemo.main(BaiLiArrayListDemo.java:17)           

上面問題的解決方案跟asList同樣,直接new一個新的ArrayList裝Arrays.subList傳回資料就可以了。

import java.util.ArrayList;
import java.util.List;

/**
 * ArrayList.subList常見問題
 * @author 百裡
 */
public class BaiLiArrayListDemo {
    public static void main(String[] args) {
        List<String> names = new ArrayList<String>() {{
            add("one");
            add("two");
            add("three");
        }};
        List strings = new ArrayList<>(names.subList(0, 1));
        strings.add("four");
        System.out.println(strings);//[one, four]
        System.out.println(names);//[one, two, three]
    }
}           

5. ArrayList中的subList切片造成OOM

subList所産生的List,其實是對原來List對象的引用,這個産生的List隻是原來List對象的視圖,也就是說雖然值切片擷取了一小段資料,但是原來的List對象卻得不到回收,如果這個原來的對象很大,就會出現OOM的情況。我們将VM參數調小:-Xms20m -Xmx40m

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * ArrayList.subList常見問題
 * @author 百裡
 */
public class BaiLiArrayListDemo {
    public static void main(String[] args) {
        List data = new ArrayList<>();
        IntStream.range(0, 1000).forEach(i ->{
            List<Integer> collect = 
                    IntStream.range(0, 100000).boxed().
                            collect(Collectors.toList());
            data.add(collect.subList(0, 1));
        });
    }
}           

出現OOM的原因:原數組無法被回收,會一直在記憶體中。

解決方案:new一個新的ArrayList接收subList傳回。

6.Copy-On-Write 是什麼?

Copy-On-Write它是一種在計算機科學中常見的優化技術,主要應用于需要頻繁讀取但很少修改的資料結構上。

簡單的說就是在計算機中就是當你想要對一塊記憶體進行修改時,我們不在原有記憶體塊中進行寫操作,而是将記憶體拷貝一份,在新的記憶體中進行寫操作,寫完之後呢,就将指向原來記憶體指針指向新的記憶體,原來的記憶體就可以被回收掉了!

既然是一種優化政策,我們看一段代碼:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

/**
* @author 百裡
*/
public class BaiLiIteratorTest {
    private static List<String> list = new ArrayList<>();

    public static void main(String[] args) {
        list.add("1");
        list.add("2");
        list.add("3");
        Iterator<String> iter = list.iterator();
        while (iter.hasNext()) {
            System.err.println(iter.next());
        }
        System.err.println(Arrays.toString(list.toArray()));
    }
}           

上面的Demo在單線程下執行時沒什麼毛病,但是在多線程的環境中,就可能出異常,為什麼呢?

因為多線程疊代時如果有其他線程對這個集合list進行增減元素,會抛出java.util.ConcurrentModificationException的異常。

我們以增加元素為例子,運作下面這Demo:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 并發疊代器問題示例代碼
 * @author 百裡
 */
public class BaiLiConcurrentIteratorTest {
    // 建立一個ArrayList對象
    private static List<String> list = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        // 給ArrayList添加三個元素:"1"、"2"和"3"
        list.add("1");
        list.add("2");
        list.add("3");

        // 開啟線程池,送出10個線程用于在list尾部添加5個元素"121"
        ExecutorService service = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            service.execute(() -> {
                for (int j = 0; j < 5; j++) {
                    list.add("121");
                }
            });
        }

        // 使用Iterator疊代器周遊list并輸出元素值
        Iterator<String> iter = list.iterator();
        for (int i = 0; i < 10; i++) {
            service.execute(() -> {
                while (iter.hasNext()) {
                    System.err.println(iter.next());
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        service.shutdown();
    }
}           

這裡暴露的問題是什麼呢?

  • 多線程場景下疊代器周遊集合的讀取操作和其他線程對集合進行寫入操作會導緻出現并發修改異常

解決方案:

  • CopyOnWriteArrayList避免了多線程操作List線程不安全的問題

7.CopyOnWriteArrayList介紹

從JDK1.5開始Java并發包裡提供了兩個使用CopyOnWrite機制實作的并發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并發場景中使用到。

CopyOnWriteArrayList原理:

在寫操作(add、remove等)時,不直接對原資料進行修改,而是先将原資料複制一份,然後在新複制的資料上執行寫操作,最後将原資料引用指向新資料。這樣做的好處是讀操作(get、iterator等)可以不加鎖,因為讀取的資料始終是不變的。

接下來我們就看下源碼怎麼實作的。

8.CopyOnWriteArrayList簡單源碼解讀

add()方法源碼:

/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
    final ReentrantLock lock = this.lock;//重入鎖
	lock.lock();//加鎖啦
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);//拷貝新數組
        newElements[len] = e;
        setArray(newElements);//将引用指向新數組  1
        return true;
    } finally {
        lock.unlock();//解鎖啦
    }
}           

可以看到,CopyOnWriteArrayList中的寫操作都需要先擷取鎖,然後再将目前的元素數組複制一份,并在新複制的元素數組上執行寫操作,最後将數組引用指向新數組。

@SuppressWarnings("unchecked")
public E next() {
    if (! hasNext()) //是否存在下一個元素
        throw new NoSuchElementException(); //沒有下一個元素,則會抛出NoSuchElementException異常
    //snapshot是一個類成員變量,它是在建立疊代器時通過複制集合内容而獲得的一個數組。
    //cursor是另一個類成員變量,初始值為0,并在每次調用next()時自增1,表示目前傳回元素的位置。
    return (E) snapshot[cursor++];
}           

而讀操作不需要加鎖,直接傳回目前的元素數組即可。

這種寫時複制的機制保證了讀操作的線程安全性,但是會犧牲一些寫操作的性能,因為每次修改都需要複制一份數組。是以,适合讀遠多于寫的場合。

是以我們将多線程Demo中的ArrayList改為CopyOnWriteArrayList,執行就不會報錯啦!

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* 并發疊代器問題示例代碼
* @author 百裡
*/
public class BaiLiConcurrentIteratorTest {
    // 建立一個ArrayList對象
    private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        // 給ArrayList添加三個元素:"1"、"2"和"3"
        list.add("1");
        list.add("2");
        list.add("3");

        // 開啟線程池,送出10個線程用于在list尾部添加5個元素"121"
        ExecutorService service = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            service.execute(() -> {
                for (int j = 0; j < 5; j++) {
                    list.add("121");
                }
            });
        }

        // 使用Iterator疊代器周遊list并輸出元素值
        Iterator<String> iter = list.iterator();
        for (int i = 0; i < 10; i++) {
            service.execute(() -> {
                while (iter.hasNext()) {
                    System.err.println(iter.next());
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        service.shutdown();
    }
}           

9.CopyOnWriteArrayList優缺點

優點:

  1. 線程安全。CopyOnWriteArrayList是線程安全的,由于寫操作對原資料進行複制,是以寫操作不會影響讀操作,讀操作可以不加鎖,降低了并發沖突的機率。
  2. 不會抛出ConcurrentModificationException異常。由于讀操作周遊的是不變的數組副本,是以不會抛出ConcurrentModificationException異常。

缺點:

  1. 寫操作性能較低。由于每一次寫操作都需要将元素複制一份,是以寫操作的性能較低。
  2. 記憶體占用增加。由于每次寫操作都需要建立一個新的數組副本,是以記憶體占用會增加,特别是當集合中有大量資料時,記憶體占用較高。
  3. 資料一緻性問題。由于讀操作周遊的是不變的數組副本,是以在對數組執行寫操作期間,讀操作可能讀取到舊的數組資料,這就涉及到資料一緻性問題。

10.CopyOnWriteArrayList使用場景

  • 讀多寫少。為什麼?因為寫的時候會複制新集合
  • 集合不大。為什麼?因為寫的時候會複制新集合
  • 實時性要求不高。為什麼,因為有可能會讀取到舊的集合資料

繼續閱讀