天天看點

Java中含有泛型的 JSON 反序列化問題

點選上方 "程式設計技術圈"關注, 星标或置頂一起成長

背景回複“大禮包”有驚喜禮包!

每日英文

We all have moments of desperation. But if we can face them head on, that’s when we find out just how strong we really are. 

我們都有絕望的時候,隻有在勇敢面對時,我們才知道我們有多堅強。

每日掏心話

有時候突然就心情很低落,不想說話也不想動。别人問起,也不知道該怎樣回答。

責編:樂樂 | 來自:明明如月學長連結:blog.csdn.net/w605283073/article/details/107350113
           

程式設計技術圈(ID:study_tech)第 1223 次推文

往日回顧:知名網站SWAG,因色情内容被警方查封!

   正文   

一、背景今天無聊之園提了一個問題,涉及的示例大緻如下:
public static void main(String[] args) {

    String jsonString = "[\"a\",\"b\"]";
    List<String> list = JSONObject.parseObject(jsonString, List.class);
    System.out.println(list);
}
例子中使用fastjson 的類庫。
為什麼 IDEA 會給出下面的警告,該如何解決?
有些同學說直接使用抑制注解,抑制掉這個警告就好了。
抑制掉警告就可以了????
二、分析2.1 事出詭異必有妖IDEA 不會無緣無故給出警告提示,警告的原因上圖已經給出。
把不帶泛型的 List 指派給帶泛型的 List, Java 編譯器并不知道右側傳回不帶泛型的實際 List 是否符合帶泛型的 List 限制。
和下面的例子非常類似:
public static void main(String[] args) {
       List first = new ArrayList();
       first.add(1);
       first.add("2");
       first.add('3');

       // 提示上述警告
       List<String> third = first;
       System.out.println(third);
}
将 first 指派給 third 時,不能保證 first 元素符合 List的限制,即清單中全是 String。
如果你執行上述代碼,會發現沒有報錯,哈哈。
但是如果你使用 foreach 循環或者疊代器取 String 循環時會發生類型轉換異常。
public static void main(String[] args) {
       List first = new ArrayList();
       first.add(1);
       first.add("2");
       first.add('3');

       List<String> third = first;
       for (String each : third) { // 類型轉換異常
           System.out.println(each);
       }
}
類型轉換異常?
我們使用 IDEA 的 jclasslib 反編譯插件,得到 main 函數的 Code 如下:
 0 new #2 <java/util/ArrayList>
 3 dup
 4 invokespecial #3 <java/util/ArrayList.<init>>
 7 astore_1
 8 aload_1
 9 iconst_1
10 invokestatic #4 <java/lang/Integer.valueOf>
13 invokeinterface #5 <java/util/List.add> count 2
18 pop
19 aload_1
20 ldc #6 <2>
22 invokeinterface #5 <java/util/List.add> count 2
27 pop
28 aload_1
29 bipush 51
31 invokestatic #7 <java/lang/Character.valueOf>
34 invokeinterface #5 <java/util/List.add> count 2
39 pop
40 aload_1
41 astore_2
42 aload_2
43 invokeinterface #8 <java/util/List.iterator> count 1
48 astore_3
49 aload_3
50 invokeinterface #9 <java/util/Iterator.hasNext> count 1
55 ifeq 79 (+24)
58 aload_3
59 invokeinterface #10 <java/util/Iterator.next> count 1
64 checkcast #11 <java/lang/String>
67 astore_4
69 getstatic #12 <java/lang/System.out>
72 aload_4
73 invokevirtual #13 <java/io/PrintStream.println>
76 goto 49 (-27)
79 return
從 42 到76 行 對應 foreach 循環的邏輯,可以看出底層使用 List 的疊代器進行周遊,取出每個元素後強轉為 String 類型,存儲到局部變量表索引為 4 的位置,然後進行列印。
在公衆号後端架構師背景回複“架構整潔”,擷取一份驚喜禮包。
如果對反編譯不熟悉可以去 target 目錄,輕按兩下編譯後的class 檔案,使用 IDEA 自帶的插件進行反編譯:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.chujianyun.common.json;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class JsonGenericDemo {
    public JsonGenericDemo() {
    }

    public static void main(String[] args) {
        List first = new ArrayList();
        first.add(1);
        first.add("2");
        first.add('3');
        List<String> third = first;
        Iterator var3 = first.iterator();

        while(var3.hasNext()) {
            String each = (String)var3.next();
            System.out.println(each);
        }
    }
}
印證了上述說法,顯然在 String each = (String)var3.next(); 這裡出現了類型轉換異常。
三、解決之道3.1 猜想驗證我們猜測是不是可以通過某種途徑将泛型作為參數傳給 fastjson, 讓 fastjson 某個傳回值是帶泛型的,進而解決這個告警呢?
顯然我們要去源碼中尋找, 在 JSONObject 類中找到了下面的方法:
/**
 * <pre>
 * String jsonStr = "[{\"id\":1001,\"name\":\"Jobs\"}]";
 * List<Model> models = JSON.parseObject(jsonStr, new TypeReference<List<Model>>() {});
 * </pre>
 * @param text json string
 * @param type type refernce
 * @param features
 * @return
 */
@SuppressWarnings("unchecked")
public static <T> T parseObject(String text, TypeReference<T> type, Feature... features) {
    return (T) parseObject(text, type.type, ParserConfig.global, DEFAULT_PARSER_FEATURE, features);
}
該函數的注釋上還貼心地給出了相關用法,是以我們改造下:
public static void main(String[] args) {
        String jsonString = "[\"a\",\"b\"]";
        List<String> list = JSONObject.parseObject(jsonString, new TypeReference<List<String>>() {
        });
        System.out.println(list);
}
警告解除了。
是以大功告成?
難道上述做法僅僅是為了消除一個警告,滿足強迫症們的心願而已嗎??
且慢,我們看下面的例子:
import lombok.Data;

@Data
public class User {
    private Long id;

    private String name;
}
mport com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import java.util.ArrayList;
import java.util.List;

public class JsonGenericDemo {

    public static void main(String[] args) {
        // 構造資料
        User user = new User();
        user.setId(0L);
        user.setName("tom");

        List<User> users = new ArrayList<>();
        users.add(user);
        // 轉為JSON字元串
        String jsonString = JSON.toJSONString(users);

        // 反序列化
        List<User> usersGet = JSONObject.parseObject(jsonString, List.class);

        for (User each : usersGet) {
            System.out.println(each);
        }
    }

}
大家執行上述例子會出現類型轉換異常!
Exception in thread “main” java.lang.ClassCastException: com.alibaba.fastjson.JSONObject cannot be cast to com.chujianyun.common.json.User at com.chujianyun.common.json.JsonGenericDemo.main(JsonGenericDemo.java:26)
有了第二部分的分析,大家可能就可以比較容易地想到
JSONObject.parseObject(jsonString, List.class) 構造出來的 List 存放的是 JSONObject 元素, foreach 循環底層使用疊代器周遊每個元素并強轉為 User 類型是報類型轉換異常。
那麼為啥 fastjson 不能幫我們轉換為 List<User> 類型呢?
有人說“由于泛型擦除,沒有泛型資訊,是以無法逆向構造回原有類型”。
其實看下 JSONObject.parseObject(jsonString, List.class); 第一個參數是字元串,第二個參數是 List.class。壓根就沒有提供泛型資訊給 fastjson。
作為這個工具函數本身,怎麼猜得到要 List 裡面究竟該存放啥類型呢?
是以如果能夠通過某種途徑,告訴它泛型的類型,就可以幫助你反序列化成真正的類型。
使用 JSONObject.parseObject(jsonString, new TypeReference<List<User>>() { }); 即可。
是以我們使用 TypeReference 并不僅僅是為了消除警告,而是為了告知 fastjson 泛型的具體類型,正确反序列化泛型的類型。
那麼底層原理是啥呢?我們看下com.alibaba.fastjson.TypeReference#TypeReference()
/**
 * Constructs a new type literal. Derives represented class from type
 * parameter.
 *
 * <p>Clients create an empty anonymous subclass. Doing so embeds the type
 * parameter in the anonymous class's type hierarchy so we can reconstitute it
 * at runtime despite erasure.
 */
protected TypeReference(){
   // 擷取父類的 Type
    Type superClass = getClass().getGenericSuperclass();

  // 如果父類是參數化類型,會傳回 java.lang.reflect.ParameterizedType
  // 調用 getActualTypeArguments 擷取實際類型的數組 并拿到第一個
    Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0];

  // 緩存中有優先取緩存,沒有則存入并設定
    Type cachedType = classTypeCache.get(type);
    if (cachedType == null) {
        classTypeCache.putIfAbsent(type, type);
        cachedType = classTypeCache.get(type);
    }

    this.type = cachedType;
}
通過代碼和注釋我們了解到:
建立一個空的匿名子類。将類型參數嵌入到匿名繼承結構中,即使運作時類型擦除也可以重建。
再回到 parseObject 函數,可以看到底層用的就是這個 type。
/**
 * <pre>
 * String jsonStr = "[{\"id\":1001,\"name\":\"Jobs\"}]";
 * List<Model> models = JSON.parseObject(jsonStr, new TypeReference<List<Model>>() {});
 * </pre>
 * @param text json string
 * @param type type refernce
 * @param features
 * @return
 */
@SuppressWarnings("unchecked")
public static <T> T parseObject(String text, TypeReference<T> type, Feature... features) {
    return (T) parseObject(text, type.type, ParserConfig.global, DEFAULT_PARSER_FEATURE, features);
}
3.2 舉一反三很多其他架構也會采用類似的方法來擷取泛型類型。
大家可以看看其他 gson 類庫
<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.8.6</version>
</dependency>
看看其中的 com.google.gson.reflect.TypeToken 類,是不是似曾相識呢?
此外,如果我們自己除了 JSON反序列化場景之外也有類似擷取泛型參數的需求,是不是也可以采用類似的方法呢?
四、總結希望大家能夠重視 IDEA 的警告。
遇到問題能夠從更合理的角度思考,了解問題的本質。
學習一個問題可以嘗試舉一反三,活學活用。
PS:歡迎在留言區留下你的觀點,一起讨論提高。如果今天的文章讓你有新的啟發,歡迎轉發分享給更多人。版權申明:内容來源網絡,版權歸原創者所有。除非無法确認,我們都會标明作者及出處,如有侵權煩請告知,我們會立即删除并表示歉意。謝謝!

歡迎加入後端架構師交流群,在背景回複“學習”即可。

最近面試BAT,整理一份面試資料《Java面試BAT通關手冊》,覆寫了Java核心技術、JVM、Java并發、SSM、微服務、資料庫、資料結構等等。在這裡,我為大家準備了一份2021年最新最全BAT等大廠Java面試經驗總結。
别找了,想擷取史上最簡單的Java大廠面試題學習資料
掃下方二維碼回複「面試」就好了


猜你還想看
阿裡、騰訊、百度、華為、京東最新面試題彙集
最牛逼的 Java 日志架構,性能無敵,橫掃所有對手。。

學會這 11 條,你離 Git 大神就不遠了!
悲痛!中南大學一碩士生墜樓身亡,生前聊天記錄被爆出,校方回應!
嘿,你在看嗎?