讀了這一篇,讓你少踩 ArrayList 的那些坑

風的姿態發表於2020-05-29

我是風箏,公眾號「古時的風箏」,一個不只有技術的技術公眾號,一個在程式圈混跡多年,主業 Java,另外 Python、React 也玩兒的 6 的斜槓開發者。
Spring Cloud 系列文章已經完成,可以到 我的github 上檢視系列完整內容。也可以在公眾號內回覆「pdf」獲取我精心製作的 pdf 版完整教程。

請看下面的程式碼,誰能看出它有什麼問題嗎?

String a = "古時的";
String b  = "風箏";
List<String> stringList = Arrays.asList(a,b);
stringList.add("!!!");

這是一個小白程式設計師問我的問題。

他說:成哥,幫我看看這程式碼有什麼問題嗎,為什麼報錯呢,啥操作都沒有啊?

我:看上去確實沒什麼問題,但是我確實沒用過 Arrays.asList這個方法,報什麼錯誤?

他:異常資訊是 java.lang.UnsupportedOperationException,是呼叫 add 方法時丟擲的。

恩,我大概明白了,這可能是 ArrayList的又一個坑,和 subList應該有異曲同工之妙。

Arrays.asList

Arrays.asList 方法接收一個變長泛型,最後返回 List,好像是個很好用的方法啊,有了它,我們總是說的 ArrayList 初始化方式是不是就能更優雅了,既不用{{這種雙括號方式,也不用先 new ArrayList,然後再呼叫 add方法一個個往裡加了。但是,為啥沒有提到這種方式呢?

雖然問題很簡單,但還是有必要看一下原因的。於是,寫了上面這 4 行程式碼做個測試,執行起來確實拋了異常,異常如下:

直接看原始碼吧,定位到 Arrays.asList 方法看一看。

public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

咦,是 new 了一個 ArrayList出來呀,怎麼會不支援 add操作呢,不仔細看還真容易被唬住,此ArrayList非彼ArrayList,這是一個內部類,但是類名也叫 ArrayList,你說坑不坑。

private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable {
  
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }

        @Override
        public int size() {
            return a.length;
        }

        @Override
        public Object[] toArray() {
            return a.clone();
        }

        @Override
        @SuppressWarnings("unchecked")
        public <T> T[] toArray(T[] a) {
            int size = size();
            if (a.length < size)
                return Arrays.copyOf(this.a, size,
                                     (Class<? extends T[]>) a.getClass());
            System.arraycopy(this.a, 0, a, 0, size);
            if (a.length > size)
                a[size] = null;
            return a;
        }

        @Override
        public E get(int index) {
            return a[index];
        }

        @Override
        public E set(int index, E element) {
            E oldValue = a[index];
            a[index] = element;
            return oldValue;
        }

        @Override
        public int indexOf(Object o) {
            E[] a = this.a;
            if (o == null) {
                for (int i = 0; i < a.length; i++)
                    if (a[i] == null)
                        return i;
            } else {
                for (int i = 0; i < a.length; i++)
                    if (o.equals(a[i]))
                        return i;
            }
            return -1;
        }

        @Override
        public boolean contains(Object o) {
            return indexOf(o) != -1;
        }

        @Override
        public Spliterator<E> spliterator() {
            return Spliterators.spliterator(a, Spliterator.ORDERED);
        }

        @Override
        public void forEach(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            for (E e : a) {
                action.accept(e);
            }
        }

        @Override
        public void replaceAll(UnaryOperator<E> operator) {
            Objects.requireNonNull(operator);
            E[] a = this.a;
            for (int i = 0; i < a.length; i++) {
                a[i] = operator.apply(a[i]);
            }
        }

        @Override
        public void sort(Comparator<? super E> c) {
            Arrays.sort(a, c);
        }
}

裡面定義了 setget等基本的方法,但是沒有重寫add方法,這個類也是繼承了 AbstractList,但是 add方法並沒有具體的實現,而是拋了異常出來,具體的邏輯需要子類自己去實現的。

public void add(int index, E element) {
    throw new UnsupportedOperationException();
}

所以說,Arrays.asList方法建立出來的 ArrayList 和真正我們平時用的 ArrayList只是繼承自同一抽象類的兩個不同子類,而 Arrays.asList建立的 ArrayList 只能做一些簡單的檢視使用,不能做過多操作,所以 ArrayList的幾種初始化方式裡沒有 Arrays.asList這一說。

subList 方法

上面提到了那個問題和 subList的坑有異曲同工之妙,都是由於返回的物件並不是真正的 ArrayList型別,而是和 ArrayList整合同一父類的不同子類而已。

坑之一

所以會產生第一個坑,就是把當把 subList返回的物件轉換成 ArrayList 的時候

List<String> stringList = new ArrayList<>();
stringList.add("我");
stringList.add("是");
stringList.add("風箏");
List<String> subList = (ArrayList) stringList.subList(0, 2);

會丟擲下面的異常:

java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList

原因很明瞭,因為這倆根本不是一個物件,也不存在繼承關係,如果真說有什麼關係,頂多算是兄弟關係,因為都繼承了 AbstractList 嘛 。

坑之二

當你在 subList 中操作的時候,其實就是在操作原始的 ArrayList,不明所以的同學以為這是一個副本列表,然後在 subList 上一頓操作猛如虎,最後回頭一看原始 ArrayList已然成了二百五。

例如下面這段程式碼,在 subList 上新增了一個元素,然後又刪除了開頭的一個元素,結果回頭一看原始的 ArrayList,發現它的結果也發生了變化。

List<String> stringList = new ArrayList<>();
stringList.add("我");
stringList.add("是");
stringList.add("風箏");
List<String> subList = stringList.subList(0, 3);
subList.add("!!!");
subList.remove(0);
System.out.println("------------------");
System.out.println("修改後的 subList");
System.out.println("------------------");
for (String s : subList) {
    System.out.println(s);
}
System.out.println("------------------");
System.out.println("原始 ArrayList");
System.out.println("------------------");
for (String a : stringList) {
    System.out.println(a);
}

以上程式碼的輸出結果:

------------------
修改後的 subList
------------------
是
風箏
!!!
------------------
原始 ArrayList
------------------
是
風箏
!!!

為什麼會發生這樣的情況呢,因為 subList的實現就是這樣子啊,捂臉。我們可以看一下 subList 這個方法的原始碼。

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

看到它內部是 new 了一個 SubList 類,這個類就是上面提到的 ArrayList的子類,看到第一個引數 this了嗎,this就是當前的 ArrayList 原始列表,之後的增刪改其實都是在 this上操作,最終也就是在原始列表上進行的操作,所以你的一舉一動最後都會誠實的反應到原始列表上,之後你再想用原始列表,對不起,已經找不到了。

坑之三

如果你使用 subList 方法獲取了一個子列表,這之後又在原始列表上進行了新增或刪除的操作,這是,你之前獲取到的 subList 就已經廢掉了,不能用了,不能用的意思就是你在 subList 上進行遍歷、增加、刪除操作都會丟擲異常,沒錯,連遍歷都不行了。

例如下面這段程式碼

List<String> stringList = new ArrayList<>();
stringList.add("我");
stringList.add("是");
stringList.add("風箏");

List<String> subList = stringList.subList(0, 3);
// 原始列表元素個數改變
stringList.add("!!!");

// 遍歷 subList
for (String s : subList) {
    System.out.println(s);
}

// get 元素
subList.get(0);

// remove 元素
subList.remove(0);

//增加元素
subList.add("hello");

遍歷、get、remove、add 都會丟擲以下異常

其實與二坑的原因相同,subList 其實操作的是原始列表,當你在 subList 上進行操作時,會執行 checkForComodification方法,此方法會檢查原始列表的個數是否和最初的相同,如果不相同,直接丟擲 ConcurrentModificationException異常。

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

最後

沒有在專案中踩過 JDK 坑的程式設計師,不足以談人生。所以,各位同學在使用一些看似簡單、優雅的方法時,一定要清楚它的特性和原理,不然就離坑不遠了。


壯士且慢,先給點個推薦吧,總是被白嫖,身體吃不消!

我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程式設計師鼓勵師,一個本打算寫詩卻寫起了程式碼的田園碼農!你可選擇現在就關注我,或者看看歷史文章再關注也不遲。

相關文章