泛型的型別擦除後,fastjson反序列化時如何還原?

碼農談IT 發表於 2022-12-08

原創:微信公眾號 碼農參上,歡迎分享,轉載請保留出處。

哈嘍大家好啊,我是Hydra~ 在前面的文章中,我們講過Java中泛型的型別擦除,不過有小夥伴在後臺留言提出了一個問題,帶有泛型的實體的反序列化過程是如何實現的,今天我們就來看看這個問題。

鋪墊

我們選擇fastjson來進行反序列化的測試,在測試前先定義一個實體類:

@Data
public class Foo<T> {
    private String val;
    private T obj;
}

如果大家對泛型的型別擦除比較熟悉的話,就會知道在編譯完成後,其實在類中是沒有泛型的。我們還是用Jad反編譯一下位元組碼檔案,可以看到沒有型別限制的T會被直接替換為Object型別:

泛型的型別擦除後,fastjson反序列化時如何還原?

下面使用fastjson進行反序列化,先不指定Foo中泛型的型別:

public static void main(String[] args) {
    String jsonStr = "{\"obj\":{\"name\":\"Hydra\",\"age\":\"18\"},\"val\":\"str\"}";
    Foo<?> foo = JSONObject.parseObject(jsonStr, Foo.class);
    System.out.println(foo.toString());
    System.out.println(foo.getObj().getClass());
}

檢視執行結果,很明顯fastjson不知道要把obj裡的內容反序列化成我們自定義的User型別,於是將它解析成了JSONObject型別的物件。

Foo(val=str, obj={"name":"Hydra","age":"18"})
class com.alibaba.fastjson.JSONObject

那麼,如果想把obj的內容對映為User實體物件應該怎麼寫呢?下面先來示範幾種錯誤寫法。

錯誤寫法1

嘗試在反序列化時,直接指定Foo中的泛型為User

Foo<User> foo = JSONObject.parseObject(jsonStr, Foo.class);
System.out.println(foo.toString());
System.out.println(foo.getObj().getClass());

結果會報型別轉換的錯誤,JSONObject不能轉成我們自定義的User

Exception in thread "main" java.lang.ClassCastException: com.alibaba.fastjson.JSONObject cannot be cast to com.hydra.json.model.User
	at com.hydra.json.generic.Test1.main(Test1.java:24)

錯誤寫法2

再試試使用強制型別轉換:

Foo<?> foo =(Foo<User>) JSONObject.parseObject(jsonStr, Foo.class);
System.out.println(foo.toString());
System.out.println(foo.getObj().getClass());

執行結果如下,可以看到,泛型的強制型別轉換雖然不會報錯,但是同樣也沒有生效。

Foo(val=str, obj={"name":"Hydra","age":"18"})
class com.alibaba.fastjson.JSONObject

好了,現在請大家忘記上面這兩種錯誤的使用方法,程式碼中千萬別這麼寫,下面我們看正確的寫法。

正確寫法

在使用fastjson時,可以藉助TypeReference完成指定泛型的反序列化:

public class TypeRefTest {
    public static void main(String[] args) {
        String jsonStr = "{\"obj\":{\"name\":\"Hydra\",\"age\":\"18\"},\"val\":\"str\"}";
        Foo foo2 = JSONObject.parseObject(jsonStr, new TypeReference<Foo<User>>(){});
        System.out.println(foo2.toString());
        System.out.println(foo2.getObj().getClass());
    }
}

執行結果:

Foo(val=str, obj=User(name=Hydra, age=18))
class com.hydra.json.model.User

Foo中的obj型別為User,符合我們的預期。下面我們就看看,fastjson是如何藉助TypeReference完成的泛型型別擦除後的還原。

TypeReference

回頭再看一眼上面的程式碼中的這句:

Foo foo2 = JSONObject.parseObject(jsonStr, new TypeReference<Foo<User>>(){});

重點是parseObject方法中的第二個引數,注意在TypeReference<Foo<User>>()有一對大括號{}。也就是說這裡建立了一個繼承了TypeReference的匿名類的物件,在編譯完成後的專案target目錄下,可以找到一個TypeRefTest$1.class位元組碼檔案,因為匿名類的命名規則就是主類名+$+(1,2,3……)

反編譯這個檔案可以看到這個繼承了TypeReference的子類:

static class TypeRefTest$1 extends TypeReference
{
    TypeRefTest$1()
    {
    }
}

我們知道,在建立子類的物件時,子類會預設先呼叫父類的無參構造方法,所以看一下TypeReference的構造方法:

protected TypeReference(){
    Type superClass = getClass().getGenericSuperclass();
    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;
}

其實重點也就是前兩行程式碼,先看第一行:

Type superClass = getClass().getGenericSuperclass();

雖然這裡是在父類中執行的程式碼,但是getClass()得到的一定是子類的Class物件,因為getClass()方法獲取到的是當前執行的例項自身的Class,不會因為呼叫位置改變,所以getClass()得到的一定是TypeRefTest$1

獲取當前物件的Class後,再執行了getGenericSuperclass()方法,這個方法與getSuperclass類似,都會返回直接繼承的父類。不同的是getSuperclas沒有返回泛型引數,而getGenericSuperclass則返回了包含了泛型引數的父類。

再看第二行程式碼:

Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0];

首先將上一步獲得的Type強制型別轉換為ParameterizedType引數化型別,它是泛型的一個介面,例項則是繼承了它的ParameterizedTypeImpl類的物件。

ParameterizedType中定義了三個方法,上面程式碼中呼叫的getActualTypeArguments()方法就用來返回泛型型別的陣列,可能返回有多個泛型,這裡的[0]就是取出了陣列中的第一個元素。

驗證

好了,明白了上面的程式碼的作用後,讓我們通過debug來驗證一下上面的過程,執行上面TypeRefTest的程式碼,檢視斷點中的資料:

泛型的型別擦除後,fastjson反序列化時如何還原?

這裡發現一點問題,按照我們上面的分析,講道理這裡父類TypeReference的泛型應該是Foo<User>啊,為什麼會出現一個List<String>

彆著急,讓我們接著往下看,如果你在TypeReference的無參構造方法中加了斷點,就會發現程式碼執行中會再呼叫一次這個構造方法。

泛型的型別擦除後,fastjson反序列化時如何還原?

好了,這次的結果和我們的預期相同,父類的泛型陣列中儲存了Foo<User>,也就是說其實TypeRefTest$1繼承的父類,完成的來說應該是TypeReference<Foo<User>>,但是我們上面反編譯的檔案中因為擦除的原因沒有顯示。

那麼還有一個問題,為什麼這個構造方法會被呼叫了兩次呢?

看完了TypeReference的程式碼,終於在程式碼的最後一行讓我發現了原因,原來是在這裡先建立了一個TypeReference匿名類物件!

public final static Type LIST_STRING 
    = new TypeReference<List<String>>() {}.getType();

因此整段程式碼執行的順序是這樣的:

  • 先執行父類中靜態成員變數的定義,在這裡宣告並例項化了這個LIST_STRING,所以會執行一次TypeReference()構造方法,這個過程對應上面的第一張圖
  • 然後在例項化子類的物件時,會再執行一次父類的構造方法TypeReference(),對應上面的第二張圖
  • 最後執行子類的空構造方法,什麼都沒有幹

至於在這裡宣告的LIST_STRING,在其他地方也沒有被再使用過,Hydra也不知道這行程式碼的意義是什麼,有明白的小夥伴可以在後臺留言告訴我。

這裡在拿到了Foo中的泛型User後,後面就可以按照這個型別來反序列化了,對後續流程有興趣的小夥伴可以自己去啃啃原始碼,這裡就不展開了。

擴充套件

瞭解了上面的過程後,我們最後通過一個例子加深一下理解,以常用的HashMap作為例子:

public static void main(String[] args) {
    HashMap<String,Integer> map=new HashMap<String,Integer>();
    System.out.println(map.getClass().getSuperclass());
    System.out.println(map.getClass().getGenericSuperclass());
    Type[] types = ((ParameterizedType) map.getClass().getGenericSuperclass())
            .getActualTypeArguments();
    for (Type t : types) {
        System.out.println(t);
    }
}

執行結果如下,可以看到這裡取到的父類是HashMap的父類AbstractMap,並且取不到實際的泛型型別。

class java.util.AbstractMap
java.util.AbstractMap<K, V>
K
V

修改上面的程式碼,僅做一點小改動:

public static void main(String[] args) {
    HashMap<String,Integer> map=new HashMap<String,Integer>(){};
    System.out.println(map.getClass().getSuperclass());
    System.out.println(map.getClass().getGenericSuperclass());
    Type[] types = ((ParameterizedType) map.getClass().getGenericSuperclass())
            .getActualTypeArguments();
    for (Type t : types) {
        System.out.println(t);
    }
}

執行結果大有不同,可以看到,只是在new HashMap<String,Integer>()的後面加了一對大括號{},就可以取到泛型的型別了:

class java.util.HashMap
java.util.HashMap<java.lang.String, java.lang.Integer>
class java.lang.String
class java.lang.Integer

因為這裡例項化的是一個繼承了HashMap的匿名內部類的物件,因此取到的父類就是HashMap,並可以獲取到父類的泛型型別。

其實也可以再換一個寫法,把這個匿名內部類換成顯示宣告的非匿名的內部類,再修改一下上面的程式碼:

public class MapTest3 {
    static class MyMap extends HashMap<String,Integer>{}

    public static void main(String[] args) {
        MyMap myMap=new MyMap();
        System.out.println(myMap.getClass().getSuperclass());
        System.out.println(myMap.getClass().getGenericSuperclass());
        Type[] types = ((ParameterizedType) myMap.getClass().getGenericSuperclass())
                .getActualTypeArguments();
        for (Type t : types) {
            System.out.println(t);
        }
    }
}

執行結果與上面完全相同:

class java.util.HashMap
java.util.HashMap<java.lang.String, java.lang.Integer>
class java.lang.String
class java.lang.Integer

唯一不同的是顯式生成的內部類與匿名類命名規則不同,這裡生成的位元組碼檔案不是MapTest3$1.class,而是MapTest3$MyMap.class,在$符後面使用的是我們定義的類名。

好啦,那麼這次的填坑之旅就到這裡,我是Hydra,下期見。

作者簡介,碼農參上,一個熱愛分享的公眾號,有趣、深入、直接,與你聊聊技術。個人微信DrHydra9,歡迎新增好友,進一步交流。