泛型的型別擦除後,fastjson反序列化時如何還原?
在前面的文章中,我們講過Java中泛型的型別擦除,不過有小夥伴在後臺留言提出了一個問題,帶有泛型的實體的反序列化過程是如何實現的,今天我們就來看看這個問題。
鋪墊
我們選擇fastjson
來進行反序列化的測試,在測試前先定義一個實體類:
@Data
public class Foo<T> {
private String val;
private T obj;
}
如果大家對泛型的型別擦除比較熟悉的話,就會知道在編譯完成後,其實在類中是沒有泛型的。我們還是用Jad
反編譯一下位元組碼檔案,可以看到沒有型別限制的T
會被直接替換為Object
型別:
下面使用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
的程式碼,檢視斷點中的資料:
這裡發現一點問題,按照我們上面的分析,講道理這裡父類TypeReference
的泛型應該是Foo<User>
啊,為什麼會出現一個List<String>
?
彆著急,讓我們接著往下看,如果你在TypeReference
的無參構造方法中加了斷點,就會發現程式碼執行中會再呼叫一次這個構造方法。
好了,這次的結果和我們的預期相同,父類的泛型陣列中儲存了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
,在$
符後面使用的是我們定義的類名。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024924/viewspace-2927337/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java泛型與型別擦除Java泛型型別
- Java泛型型別擦除問題Java泛型型別
- 泛型擦除的原理泛型
- Java 泛型,你瞭解型別擦除嗎?Java泛型型別
- 面試官:說說什麼是泛型的型別擦除?面試泛型型別
- Java泛型(三):型別擦除帶來的約束與侷限性Java泛型型別
- Swift 型別擦除Swift型別
- 一句話,講清楚java泛型的本質(非型別擦除)Java泛型型別
- 初探Java型別擦除Java型別
- Java™ 教程(型別擦除)Java型別
- 【原創】【自制系列】自制stack型別(泛型)型別泛型
- [譯]Swift 中的型別擦除Swift型別
- 泛型方法的反模式泛型模式
- 泛型類序列化泛型
- 型別 VS 泛型型別泛型
- TypeScript 泛型型別TypeScript泛型型別
- 泛型類、泛型方法、型別萬用字元的使用泛型型別字元
- 泛型型別(.NET 指南)泛型型別
- 如何使用Java泛型對映不同的值型別Java泛型型別
- 從 Swift 中的序列到型別擦除Swift型別
- C# 中使物件序列化/反序列化 Json 支援使用派生型別以及泛型的方式C#物件JSON型別泛型
- Java™ 教程(泛型原始型別)Java泛型型別
- 型別與泛型標記型別泛型
- Scala 泛型型別和方法泛型型別
- TypeScript 基本型別和泛型的使用TypeScript型別泛型
- Java泛型的PECS原則Java泛型
- 泛型最佳實踐:Go泛型設計者教你如何用泛型泛型Go
- 泛型作為返回型別的寫法泛型型別
- Java中建立泛型型別的例項Java泛型型別
- 泛型--泛型萬用字元和泛型的上下限泛型字元
- rust trait 關聯型別和泛型的區別RustAI型別泛型
- Java泛型知識點:泛型類、泛型介面和泛型方法Java泛型
- C# 泛型 引用型別約束 值型別約束C#泛型型別
- 泛型類、泛型方法及泛型應用泛型
- C# 泛型集合的自定義型別排序C#泛型型別排序
- Go 官方出品泛型教程:如何開始使用泛型Go泛型
- 抖音二面栽在泛型擦除?答應我,別和我倒在同一地方!泛型
- JSON序列化時將BigDecimal型別轉換成String型別JSONDecimal型別