List里有可能存String類型元素嗎?

這其實是我遇到的一個線上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,其實就是泛型方法使用錯誤了。

toList()是公司內部提供的公共工具類方法,那麼我們如果還是要做到原來這個方法的效果,該怎麼修改這個方法呢?

問題修復

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

但我們想,如果後面又有別的同事遇到這個問題了怎麼辦呢,也會一臉懵逼,最好還是希望toList()方法拋出個java.lang.ClassCastException。所以我們這麼修改下toList()方法,增加一個參數,告訴方法你希望返回一個什麼類型的值:

這樣的話,如果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;
    }
Tags: