昨天同事開發的時候遇到了一個奇怪的問題。java
使用Guava作緩存,往裏面存一個List,爲了方便描述,稱它爲列表A,在另外一個地方取出來,再跟列表B中的元素進行差集處理,簡單來講,就像是下面這樣:redis
public class ArrayListTest { // 方便起見,這裏用HashMap來作緩存 private Map<String, List<Long>> cache = new HashMap<>(); private void save(){ List<Long> listA = createListA(); cache.put("listA", listA); } private void get(){ List<Long> listB = createListB(); List<Long> listA = cache.get("listA"); listA.removeAll(listB); } private List<Long> createListA(){ ··· } private List<Long> createListB(){ ··· } public static void main(String[] args){ ArrayListTest test = new ArrayListTest(); test.save(); test.get(); } }
先調用save方法,而後調用get方法,而後就拋出了異常:spring
Exception in thread "main" java.lang.UnsupportedOperationException at java.util.AbstractList.remove(AbstractList.java:161) at java.util.AbstractList$Itr.remove(AbstractList.java:374) at java.util.AbstractCollection.removeAll(AbstractCollection.java:376) ...
到底是人性的泯滅仍是道德的淪喪,一個小小的List居然也玩不轉了,面對突如其來的打擊,我跟同事都開始反思,複製粘貼一時爽,debug火葬場。shell
但做爲一名優秀的程序猿,怎麼能被這點困難所難倒呢?因而開始了問題排查之旅。json
先來驗證一下本身對ArrayList是否有什麼誤解:數組
@Test public void testArrayList() { List<Long> listA = new ArrayList<>(); listA.add(1L); listA.add(2L); List<Long> listB = new ArrayList<>(); listB.add(2L); listB.add(3L); listA.removeAll(listB); System.out.println(JSON.toJSONString(listA)); }
輸出以下:緩存
[1]
嗯,看來並無。dom
再回過頭看看,拋出的異常是 UnsupportedOperationException
異常,並且是在 AbstractList
裏拋出的,因而打開了 AbstractList
的源碼。ide
public E remove(int index) { throw new UnsupportedOperationException(); }
AbstractList
類對remove方法的默認實現就是直接拋出一個異常,因此若是子類並無覆蓋該方法,就會出現上面的問題。函數
那麼問題應該就出在列表A的建立方式上。
結果一找,發現列表A是經過 Arrays.asList()
建立的,再跟進代碼:
public static <T> List<T> asList(T... a) { return new ArrayList<>(a); }
感受好像也沒哪裏不對,這裏也是建立一個 ArrayList
,講道理的話,應該沒問題纔對,不過等等,ArrayList
好像沒有能傳入可變長參數的構造函數吧,因而朝着這個ArrayList
小手一點,終於發現了問題所在。
原來經過 Arrays.asList()
建立的 List
對象是經過實例化 Arrays
內部類 ArrayList
來建立的,因此這個 ArrayList
並非咱們經常使用的那個 ArrayList
。
這個內部類並無覆蓋父類 AbstractList
的 remove
方法,因此調用的時候就會直接調用父類的 remove
方法,因而便發生了上面的異常。
爲了更好的使用這裏方法,咱們先來看看它的註釋說明:
/** * Returns a fixed-size list backed by the specified array. (Changes to * the returned list "write through" to the array.) This method acts * as bridge between array-based and collection-based APIs, in * combination with {@link Collection#toArray}. The returned list is * serializable and implements {@link RandomAccess}. * * <p>This method also provides a convenient way to create a fixed-size * list initialized to contain several elements: * <pre> * List<String> stooges = Arrays.asList("Larry", "Moe", "Curly"); * </pre> * * @param <T> the class of the objects in the array * @param a the array by which the list will be backed * @return a list view of the specified array */
從說明能夠發現,有這麼幾點須要注意:
一、該方法返回的是一個固定長度的列表
因此它的長度是不能被改變的,也就不能對它進行添加和刪除元素的操做,從它的內部類ArrayList的方法列表也能夠看出,並無覆蓋add和remove方法,所以對這兩個方法的調用都會致使拋出異常。
雖然不能改變列表的長度,可是能夠改變列表中的元素,以及元素的位置。好比經過set方法來從新設值,經過replaceAll方法來批量替換,經過sort方法來排序等等。
二、任何對列表的改動都會回寫到原來是數組
也就是說對返回的列表進行的任何修改操做,都會致使原數組的改變。能夠經過一個Test來測試一下:
@Test public void testArrays() { Long[] longs = {1L,2L,4L,3L}; List<Long> longList = Arrays.asList(longs); System.out.println("longList:" + JSON.toJSONString(longList) + "longs:" + JSON.toJSONString(longs)); longList.set(1, 5L); System.out.println("longList:" + JSON.toJSONString(longList) + "longs:" + JSON.toJSONString(longs)); longList.replaceAll(a -> a + 1L); System.out.println("longList:" + JSON.toJSONString(longList) + "longs:" + JSON.toJSONString(longs)); longList.sort(Long::compareTo); System.out.println("longList:" + JSON.toJSONString(longList) + "longs:" + JSON.toJSONString(longs)); longs[2] = 7L; System.out.println("longList:" + JSON.toJSONString(longList) + "longs:" + JSON.toJSONString(longs)); }
輸出以下:
longList:[1,2,4,3]longs:[1,2,4,3] longList:[1,5,4,3]longs:[1,5,4,3] longList:[2,6,5,4]longs:[2,6,5,4] longList:[2,4,5,6]longs:[2,4,5,6] longList:[2,4,7,6]longs:[2,4,7,6]
注意最後一個輸出,咱們修改原數組的元素,也會致使列表元素的改變,究其緣由,固然是由於列表只是將數組封裝了起來而已,最終指向的都是同一個內存地址,所以修改天然也是同步的。
三、不能使用基本數據類型數組來做爲參數
舉個栗子:
@Test public void testArrays2() { int[] ints = { 1, 2, 3 }; List list = Arrays.asList(ints); System.out.println(list.size()); }
這裏並不會報錯,而是會輸出1
。爲何呢?
再回過頭去看下說明:
@param <T> the class of the objects in the array
參數的類型T指的是數組中的元素類型,若是數組中元素類型是基本類型,就會把整個數組當成一個元素,咱們把上面的栗子稍微修改一下就清楚了。
@Test public void testArrays2() { int[] ints = { 1, 2, 3 }; System.out.println(ints.getClass()); List list = Arrays.asList(ints); System.out.println(JSON.toJSONString(list)); }
輸出以下:
class [I [[1,2,3]]
注意第二行的輸出是一個二維數組。變長參數本質上就是一個對象數組,因此若是傳入一個Integer數組,就能正常接收:
@Test public void testArrays2() { Integer[] ints = { 1, 2, 3 }; System.out.println(ints.getClass()); List list = Arrays.asList(ints); System.out.println(list.size()); }
class [Ljava.lang.Integer; 3
至此,關於 Arrays.asList()
的探索之旅就結束了,遇到問題通常跟一跟源碼就差很少能解決了,但對於經常使用的類,若是對其內部的運行機制不熟悉的話,代碼就會容易出現一些不符合預期的行爲,報錯的異常並不可怕,由於能夠根據異常很快定位,最怕的就是不報錯,能正常運行,可是數據處理倒是錯誤的,等到真正發現的時候,可能已經形成了難以挽回的損失。
看來主動閱讀源碼仍是至關有必要的,其實Arrays.asList()
並不難使用,推而廣之,就像Guava、fastjson這些模塊,或者spring、redis、dubbo之類,學習使用並不難,但若是不熟悉內部運行機制,僅僅當成一個黑盒的話,沒法探索內部的精妙設計,遇到問題也比較難處理,若是隻是把功能框定在其設定的能力範圍以內,就沒有辦法進行定製化的改造。
嗯,看來個人歷練路程還很長啊。最後用荀子的一句話來共勉吧。
「路雖彌,不行不至,
事雖小,不作不成。」