1. 程式人生 > >【問題總結】萬萬沒想到,竟然栽在了List手裡

【問題總結】萬萬沒想到,竟然栽在了List手裡

說明

昨天同事開發的時候遇到了一個奇怪的問題。

使用Guava做快取,往裡面存一個List,為了方便描述,稱它為列表A,在另一個地方取出來,再跟列表B中的元素進行差集處理,簡單來說,就像是下面這樣:

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方法,然後就丟擲了異常:

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火葬場。

但作為一名優秀的程式猿,怎麼能被這點困難所難倒呢?於是開始了問題排查之旅。

先來驗證一下自己對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]

嗯,看來並沒有。

再回過頭看看,丟擲的異常是 UnsupportedOperationException 異常,而且是在 AbstractList 裡丟擲的,於是打開了 AbstractList的原始碼。

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

這個內部類並沒有覆蓋父類 AbstractListremove 方法,所以呼叫的時候就會直接呼叫父類的 remove 方法,於是便發生了上面的異常。

Arrays.asList的正確開啟方式

為了更好的使用這裡方法,我們先來看看它的註釋說明:

 /**
* 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&lt;String&gt; 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
*/

從說明可以發現,有這麼幾點需要注意:

1、該方法返回的是一個固定長度的列表

所以它的長度是不能被改變的,也就不能對它進行新增和刪除元素的操作,從它的內部類ArrayList的方法列表也可以看出,並沒有覆蓋add和remove方法,因此對這兩個方法的呼叫都會導致丟擲異常。

雖然不能改變列表的長度,但是可以改變列表中的元素,以及元素的位置。比如通過set方法來重新設值,通過replaceAll方法來批量替換,通過sort方法來排序等等。

2、任何對列表的改動都會回寫到原來是陣列

也就是說對返回的列表進行的任何修改操作,都會導致原陣列的改變。可以通過一個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]

注意最後一個輸出,我們修改原陣列的元素,也會導致列表元素的改變,究其原因,當然是因為列表只是將陣列封裝了起來而已,最終指向的都是同一個記憶體地址,因此修改自然也是同步的。

3、不能使用基本資料型別陣列來作為引數

舉個栗子:

@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之類,學習使用並不難,但如果不熟悉內部執行機制,僅僅當成一個黑盒的話,無法探索內部的精妙設計,遇到問題也比較難處理,如果只是把功能框定在其設定的能力範圍之內,就沒有辦法進行定製化的改造。

嗯,看來我的歷練路程還很長啊。最後用荀子的一句話來共勉吧。

“路雖彌,不行不至,

事雖小,不做不成。”

相關推薦

問題總結萬萬想到竟然List手裡

說明 昨天同事開發的時候遇到了一個奇怪的問題。 使用Guava做快取,往裡面存一個List,為了方便描述,稱它為列表A,在另一個地方取出來,再跟列表B中的元素進行差集處理,簡單來說,就像是下面這樣: public class ArrayListTest { // 方便起見,這裡用HashMap來做

總結歲月不回首你也別回頭 2016.09-2017.02

    我們每個人都會有一段再也回不去的歲月 【思想】     這一年成長了很多,從一個內心成熟的人逐漸轉變為一個行為上也成熟的人     這一年是我離開故土的第三年,不知道你的變化大不大     這一年我告別了又愛又恨的大學生活,隨之而來的是沒有回頭路的社會生活    

漫畫活見鬼明明刪除資料空間卻減少!

遷移資料常用 1、匯出檔案 - mysqldump 命令  ‍mysqldump 是 Mysql 自帶的邏輯備份工具。其備份原理是通過協議連線到 Mysql 資料庫,將需要備份的資料查詢出來轉換成對應的 inser

雜談一個回車下去瀏覽器做什麽?

長連接 cat 解析 編程 至少 -c 獲取 connect 異常 前言   在使用PostMan之前,自己測試Rest接口都是直接在瀏覽器地址欄輸入URL來測試的,但是這種方法發出的請求都是Get,如果要發送POST請求只能用ajax等編程方式。有了PostMan就方便多

題目:我立志成為一名好銷售萬萬想到我還是走程式設計師的路原因竟然是....

        【程式設計師養成第一步】         【叢立志當一名銷售到決定從事程式設計師的蛻變】         學習生活中,我閱讀了數不清的推送、博文,看了數不清的作者的故事,精彩的各種推送,以及各類技術大神的解答貼,為我的求學之路提供了技術上的幫助以及精神上的鼓

總結mac下配置less並在sublime中安裝sublime3中啟用錯誤

mac下配置less     https://blog.csdn.net/jiaoshenmo/article/details/51484052 1、下載安裝node.js環境 2、安裝過node.js for mac 安裝包,可以使用node和npm的命令了,npm.j

降取樣過取樣欠取樣子取樣下采樣上取樣你學會嗎?總結

降取樣:2048HZ對訊號來說是過取樣了,事實上只要訊號不混疊就好(滿足尼奎斯特取樣定理),所以可以對過取樣的訊號作抽取,即是所謂的“降取樣”。在現場中取樣往往受具體條件的限止,或者不存在300HZ的取樣率,或除錯非常困難等等。若R>>1,則Rfs/2就遠大於音

總結2016.09-2017.09 再見過去你好未來

引言        回顧一年的歷程,看著自己一點一點成長。感謝不為人知的過去,感激還沒有遇見的未來。 思想        思想上移,行動上移,是一直都在貫徹的。 技術        這一年從C/S跨到了B/S,接觸到了不同的世界。 英語        一直都沒有放棄的英語,

PHP開發經驗之談受益非淺

his 則表達式 處理 手冊 調用 緩存系統 字符串操作函數 如果能 諸多 用單引號代替雙引號來包含字符串,這樣做會更快一些。因為PHP會在雙引號包圍的字符串中搜尋變量,單引號則不會,註意:只有echo能這麽做,它是一種可以把多個字符串當作參數的“函數”(譯註:PHP手冊中

Nginx教程(7) 正向代理與反向代理總結

資料 用戶訪問 認證 origin 訪問者 發送 -128 負載 行為 1、前言   最近工作中用到反向代理,發現網絡代理的玩法還真不少,網絡背後有很多需要去學習。而在此之前僅僅使用了過代理軟件,曾經為了訪問google,使用了代理軟件,需要在瀏覽器中配置代理的地址。我只知

手摸手帶你用vue擼後臺 系列二(登錄權限篇)

userinfo ogr abort 變化 再次 狀態碼 quest -o 監聽 前言 拖更有點嚴重,過了半個月才寫了第二篇教程。無奈自己是一個業務猿,每天被我司的產品虐的死去活來,之前又病了一下休息了幾天,大家見諒。 進入正題,做後臺項目區別於做其它的項目,權限驗證與

pythonftp連接主被動調試等級

login 打開 blog pat 連接 rom down .tar.gz 服務器 示例代碼如下: #!/usr/bin/env python # -*- coding: utf-8 -*- import os from ftplib import FTP de

Python3print用逗號write用加號

nbsp int pre python3 col cda pri pytho write print("\n", Gb[u], "\t",IDlist[u],end="") f.write("\n"+ Gb[u]+"\t"+IDlist[u]) 【Python3】prin

總結spark按文本格式和Lzo格式處理Lzo壓縮文件的比較

spark lzotextinputformat1、描述spark中怎麽加載lzo壓縮格式的文件2、比較lzo格式文件以textFile方式和LzoTextInputFormat方式計算數據,Running Tasks個數的影響 a.確保lzo文件所在文件夾中生成lzo.index索引文件 b.以

測試方法總結

測試方法測試方法從測試設計方法分類測試名稱測試內容黑盒測試把軟件系統當作一個黑箱,無法了解或使用系統內部結構及知識白盒測試設計者可以看到軟件系統的內部結構,並且使用軟件的內部知識來指導測試數據及方法的選擇灰盒測試介於白盒和黑盒之間總結:在實際工作中,對系統的了解越多越好,目前大多數的測試人員都是做黑盒測試,很

javascript異步編年史從“純回調”到Promise

條件 one org 場景 存在 gofunc ges 語句 += 異步和分塊——程序的分塊執行 一開始學習javascript的時候, 我對異步的概念一臉懵逼, 因為當時百度了很多文章,但很多各種文章不負責任的把籠統的描述混雜在一起,讓我對

javajava反射機制動態獲取對象的屬性和對應的參數值並屬性按照字典序排序Field.setAccessible()方法的說明可用於微信支付 簽名生成

modifier 直接 this 字段值 1-1 讓我 toupper ima play 方法1:通過get()方法獲取屬性值 package com.sxd.test.controller; public class FirstCa{ private

總結差分約束模型的要點

cio 一個點 ros 最短路 所有 運行時間 16px net 不同   只是一些自己想到的東西,記下來以防忘記。   1. 求解一系列的 f[b] - f[a] <= x 不等式組時,由a向b建權值為x的邊,求最短路。有負環時無解,體現為在SPFA中一個點入隊

HTTP響應狀態碼總結

管理 指示 get 強制 opp 帶寬 行修改 accepted 代碼 常見的狀態碼【1XX】表示【消息】【2XX】表示【成功】【3XX】表示【重定向】【4XX】表示【請求錯誤】【5XX】表示【服務器端錯誤】200:OK。請求被正常處理204:No Content。請求被受

windowswindows系統下在任務管理器的進程選項卡中查看PID/任務管理器怎麽查看PID

分享圖片 圖片 技術 啟動 最大值 成功 9.png mage 選擇列 PID,就是windows上的進程ID,是一個進程的唯一標識值。 那今天啟動JDK跑起來一個項目之後,想要在任務管理器中查看這個JDK所在進程的PID但是看不到。 怎麽解決? 1.我在任務管理