1. 程式人生 > >讀了這一篇,讓你少踩 ArrayList 的那些坑

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

> 我是風箏,公眾號「古時的風箏」,一個不只有技術的技術公眾號,一個在程式圈混跡多年,主業 Java,另外 Python、React 也玩兒的 6 的斜槓開發者。 Spring Cloud 系列文章已經完成,可以到 [我的github](https://github.com/huzhicheng/spring-cloud-study) 上檢視系列完整內容。也可以在公眾號內回覆「pdf」獲取我精心製作的 pdf 版完整教程。 請看下面的程式碼,誰能看出它有什麼問題嗎? ```java String a = "古時的"; String b = "風箏"; List 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 行程式碼做個測試,執行起來確實拋了異常,異常如下: ![](https://tva1.sinaimg.cn/large/007S8ZIlly1gf52kpt6l9j30qk03u3zb.jpg) 直接看原始碼吧,定位到 `Arrays.asList` 方法看一看。 ```java public static List asList(T... a) { return new ArrayList<>(a); } ``` 咦,是 new 了一個 `ArrayList`出來呀,怎麼會不支援 `add`操作呢,不仔細看還真容易被唬住,此`ArrayList`非彼`ArrayList`,這是一個內部類,但是類名也叫 `ArrayList`,你說坑不坑。 ```java private static class ArrayList extends AbstractList 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[] toArray(T[] a) { int size = size(); if (a.length < size) return Arrays.copyOf(this.a, size, (Class) 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 spliterator() { return Spliterators.spliterator(a, Spliterator.ORDERED); } @Override public void forEach(Consumer action) { Objects.requireNonNull(action); for (E e : a) { action.accept(e); } } @Override public void replaceAll(UnaryOperator 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 c) { Arrays.sort(a, c); } } ``` 裡面定義了 `set`、`get`等基本的方法,但是沒有重寫`add`方法,這個類也是繼承了 `AbstractList`,但是 `add`方法並沒有具體的實現,而是拋了異常出來,具體的邏輯需要子類自己去實現的。 ```java 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` 的時候 ```java List stringList = new ArrayList<>(); stringList.add("我"); stringList.add("是"); stringList.add("風箏"); List subList = (ArrayList) stringList.subList(0, 2); ``` 會丟擲下面的異常: ```shell java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList ``` 原因很明瞭,因為這倆根本不是一個物件,也不存在繼承關係,如果真說有什麼關係,頂多算是兄弟關係,因為都繼承了 `AbstractList` 嘛 。 ### 坑之二 當你在 subList 中操作的時候,其實就是在操作原始的 `ArrayList`,不明所以的同學以為這是一個副本列表,然後在 subList 上一頓操作猛如虎,最後回頭一看原始 `ArrayList`已然成了二百五。 例如下面這段程式碼,在 subList 上新增了一個元素,然後又刪除了開頭的一個元素,結果回頭一看原始的 ArrayList,發現它的結果也發生了變化。 ```java List stringList = new ArrayList<>(); stringList.add("我"); stringList.add("是"); stringList.add("風箏"); List 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); } ``` 以上程式碼的輸出結果: ```shell ------------------ 修改後的 subList ------------------ 是 風箏 !!! ------------------ 原始 ArrayList ------------------ 是 風箏 !!! ``` 為什麼會發生這樣的情況呢,因為 `subList`的實現就是這樣子啊,捂臉。我們可以看一下 subList 這個方法的原始碼。 ```java public List 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 上進行遍歷、增加、刪除操作都會丟擲異常,沒錯,連遍歷都不行了。 例如下面這段程式碼 ```java List stringList = new ArrayList<>(); stringList.add("我"); stringList.add("是"); stringList.add("風箏"); List 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 都會丟擲以下異常 ![](https://tva1.sinaimg.cn/large/007S8ZIlly1gf5zko5dtpj30vq04cab1.jpg) 其實與二坑的原因相同,subList 其實操作的是原始列表,當你在 subList 上進行操作時,會執行 `checkForComodification`方法,此方法會檢查原始列表的個數是否和最初的相同,如果不相同,直接丟擲 `ConcurrentModificationException`異常。 ```java private void checkForComodification() { if (ArrayList.this.modCount != this.modCount) throw new ConcurrentModificationException(); } ``` ## 最後 沒有在專案中踩過 JDK 坑的程式設計師,不足以談人生。所以,各位同學在使用一些看似簡單、優雅的方法時,一定要清楚它的特性和原理,不然就離坑不遠了。 *** **壯士且慢,先給點個推薦吧,總是被白嫖,身體吃不消!** > **我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程式設計師鼓勵師,一個本打算寫詩卻寫起了程式碼的田園碼農!你可選擇現在就關注我,或者看看歷史文章再關注也不遲。** ![](https://img2020.cnblogs.com/blog/273364/202005/273364-20200529113429430-6011043