List<Integer>裡有可能存String型別元素嗎?

dijia478發表於2022-01-30

這其實是我遇到的一個線上bug,在這裡分享給大家。

如果是用反射,那就很簡單了,畢竟泛型只是在編譯期進行約束,對執行期是無能為力的。

想想看,如果不使用反射,有沒有辦法做到呢?

問題起因

在我們公司的實際業務中,有一段類似於這樣邏輯的程式碼,文章最後會放出做測試構造的getList()方法:

    /**
     * 主要業務邏輯
     */
    public static void main(String[] args) {
        // 從資料庫查詢資料列表,不用關注裡面的實現細節
        List<DataBO> list = getList();

        // 獲取所有“a”欄位的值的集合
        List<Integer> integerList = toList(list, "a");

        if (integerList.contains(1)) {
            System.out.println("集合裡包含1,處理對應的邏輯");
        } else {
            System.out.println("集合裡不包含1,處理對應的邏輯");
        }
    }

    /**
     * 這是公司提供的一個公共工具方法,獲取集合中,每個物件的某個欄位的值的集合
     *
     * @param list 資料物件集合
     * @param key 欄位
     * @return 值的集合
     */
    public static <T> List<T> toList(List<DataBO> list, String key) {
        return list.stream()
                .filter(x -> x.get(key) != null)
                .map(x -> (T)x.get(key))
                .collect(Collectors.toList());
    }

其中的DataBO物件簡化如下:

public class DataBO {

    /** 資料庫的一條資料,key是列,value是值 */
    private Map<String, Object> map = new HashMap<>();

    public Object get(String key) {
        return map.get(key);
    }

    public void set(String key, Object value) {
        map.put(key, value);
    }

    @Override
    public String toString() {
        return "DataBO{" + "map=" + map + '}';
    }

}

原本我這裡的業務需求是,取列表資料中,所有“a”欄位的值出來,判斷其中是否含有1。

已知資料庫裡“a”欄位定義為int型別,並且確認了有一條資料在“a”欄位上存的是1。但是程式碼上線一跑,出bug了。

查出來怎麼就走到“不包含1”的分支裡去了呢?也沒有報錯,難道底層服務的getList()方法有什麼特殊處理,把資料庫a=1的那條資料給過濾掉了嗎?

問題定位

於是我加了點日誌,把listintergerList的元素列印出來,看看裡面到底存了什麼東西。於是又上線一版,觀察一看,神奇的事情出現了,裡面明明有1啊??!為啥會走到下面“不包含1”的分支呢?見鬼了!

於是我只能本地debug了一下,才發現資料庫查到的集合裡,“a”欄位返回的是個字串"1"!而ArrayList的contains()方法,底層是用equals()去比較是否存在的。"1".equals(1),結果肯定是false,所以認為不存在。

好吧,雖然資料庫的“a”欄位定義為int型別,但是底層服務估計哪裡有bug,把Integer型別的欄位,轉換成了String型別返回給上層服務了。

但轉念一向,不對啊,我明明定義的是List<Integer>型別的變數,如果是這樣的話,就算查出來"a"欄位不是個Integer型別的值,那toList()方法也應該是拋個java.lang.ClassCastException才對,怎麼可能正常往下走呢?List<Integer>變數指向的物件裡,為什麼會存進去一個字串呢?為什麼toList()方法的.map(x -> (T)x.get(key))這一行沒有報錯呢?

問題解析

問題很明顯就是出在了toList()方法裡,那個強制型別轉換為泛型,並沒有生效。開頭我們說了,java的泛型,只是在編譯期進行約束,對執行期是無能為力的。那麼我們首先就應該想到的就是java的泛型擦除機制,我們對demo類進行編譯、再反編譯看看。

反編譯可以發現,原來toList()方法中,強制型別轉換被擦除了。所以返回的其實並不是List<Integer>物件,而是List物件,沒有泛型限制。很明顯是這個方法有bug,其實就是泛型方法使用錯誤了。

問題修復

本來這個線上bug到這裡就已經搞清楚了,如果只是要快速修復上線也很容易就能解決,把toList()方法返回的集合改成List<String>,然後判斷集合是否包含字串"1"就行。

但我們想,如果後面又有別的同事遇到這個問題了怎麼辦呢,也會一臉懵逼,最好還是希望toList()方法丟擲個java.lang.ClassCastException,而且還要做到原來這個方法的效果,該怎麼修改這個方法呢?

我們可以增加一個引數,告訴方法你希望返回一個什麼型別的值:

這樣的話,如果toList()方法還是返回原來的List<Integer>,就會拋異常:

而且如果前後限制的型別不一致,編譯期也會報錯,泛型就起作用了:

到此這個問題徹底解決。

補充下本文用於測試構造的getList()方法:

    /**
     * 查資料庫,獲取資料物件的集合
     *
     * @return 資料物件的集合
     */
    public static List<DataBO> getList() {
        // 這個list是從資料庫查出來的
        List<DataBO> list = new ArrayList<>();
        DataBO db1 = new DataBO();
        db1.set("a", "1");
        DataBO db2 = new DataBO();
        db2.set("a", 2);
        list.add(db1);
        list.add(db2);
        return list;
    }

相關文章