Java泛型與Json反序列化
Java的JSON庫有很多,本文分析google的Gson和alibaba的fastjson,在Java泛型場景反序列化的一些有意思的行為。考慮下面的json字串:
[ "2147483648", "2147483647" ]
用fastjson在不指定型別的情況下解析,下面的程式碼輸出啥:
JSON.parseArray(s).forEach(o -> { System.out.println(o.getClass()); });
答案是:
class java.lang.Long class java.lang.Integer
是不是感覺有點兒奇怪,兩個都是數字啊,居然輸出了不同的型別,原因我們下面細講。再看看Gson, 用Gson解析並且不指定泛型型別的話,下面的程式碼輸出啥:
new Gson().fromJson(s, List.class).forEach(o -> { System.out.println(o.getClass()); });
答案是:
class java.lang.Double class java.lang.Double
這次兩個都是Double型別,明明是整數啊,為啥到Gson這裡變成Double了?
我們試了兩個Json庫,解析相同的json字串,得到全然不同的結果。如果在實際使用的時候,用這種方式反序列化JSON,很容易出現BUG,而且是執行時才可能出現,這種問題一旦出現往往很難排查。因此為了保證泛型型別Json反序列化的正確性,一定要明確指定泛型的型別。下面我們先看看正確的解析應該怎麼寫,再深度探討一下內部的原理。
正確的泛型Json反序列化
fastjson和Gson都提供了泛型型別的反序列化方案,先來看看fastjson,對於上面的case,正確的反序列化程式碼如下:
JSON.parseObject(s, new TypeReference<List<Long>>(){}) .forEach(o -> { System.out.println(o.getClass()); });
建立一個確定泛型型別的TypeReference
子類(這裡是匿名內部類),將這個子類傳遞給fastjson以幫助fastjson在執行時獲得泛型的具體型別資訊,從而實現泛型正確反序列化。Gson的方案與fastjson相同,或者應該反過來說fastjson的方案與Gson相同。大家感興趣的話可以看看fastjson的TypeReference
和Gson的TypeToken
程式碼,基本上fastjson就是抄襲Gson,連註釋都抄了......。Gson指定泛型型別的反序列化方法如下,也是建立一個確定泛型型別的匿名子類:
new Gson().<List<Long>>fromJson(s, new TypeToken<List<Long>>(){}.getType()) .forEach(o -> { System.out.println(o.getClass()); });
由於fastjson的方案是來自Gson,以下只討論Gson的泛型反序列化原理。為什麼泛型的反序列化顯得這麼麻煩呢,非要通過子類化的方式,不能直接告訴Gson泛型的型別嗎?是的,Java是真的做不到,其實理由很簡單,泛型類既然是泛型,意味著就不應該帶有具體泛型型別的資訊,因此泛型類的位元組碼本身就不應該也無法儲存泛型型別,但是子類如果繼承一個明確泛型型別的父類(父類是一個泛型型別,Gson裡面就是TypeToken),子類必須儲存父類的明確型別資訊,通過Class
類的getGenericSuperclass
方法能夠獲得父類型別資訊,該型別資訊包含了父類具體的泛型型別資訊。這個方法幫助Java的泛型體系完整化了,是非常重要的一個方法。
類庫設計分析
面對相同的設計問題,fastjson與Gson在很多點上的選擇不同,藉此機會可以一窺類庫設計的思想,讓我們一一來看。
是靜態方法還是例項化
使用Gson之前,必須進行例項化,Gson提供了兩種方式:一種是無引數構造器,一種是通過GsonBuilder,後者能夠進行更多的定製,但無論是哪種方法,都需要例項化一個Gson物件。但是Fastjson使用之前是不需要例項化的,直接使用JSON類的靜態方法即可實現json序列化和反序列化。這一點上來講,Fastjson比較方便,雖然Gson是執行緒安全的,可以用static變數來宣告一個Gson例項(餓漢模式的單例)然後全域性使用,但是還是比Fastjson多了一步。但是Gson這麼做的好處是如果序列化(反序列化)的定製比較多,可以在初始化的時候完成複雜的擴充套件定製,使用的時候依然保持簡單,Fastjson就需要每次都傳遞額外的引數來實現。總體來講各有優化,Gson是執行緒安全的,大部分場景都是定義全域性的靜態單例,用起來跟Fastjson差不多。Gson在這裡的選擇傾向於希望對外的介面保持一致和簡單 ,即無論怎麼定製邏輯,Json的序列化和反序列化就那麼幾個方法,內部邏輯可以定製,但是使用介面不用改變。
數字的預設型別
對於數字型別,如果沒有明確指定型別,Gson預設都解析成Double型別,而Fastjson會根據數字的不同,解析成Long、Integer或者BigDecimal。我們在生產中用Fastjson就遇到這種問題:由於集合沒有指定泛型型別,反序列化的時候,不同大小的數字被反序列化成了不同的型別,導致業務邏輯出錯。這種未制定型別情況下,感覺Gson的處理更合適一些,既然未指定型別,對外的預設型別始終是Double,介面對外的心智更穩定。
集合型別反序列化
對於列表型別的反序列化,Fastjson提供了parseArray系列方法,這樣很多情況下可以避免使用TypeReference
,程式碼寫起來更簡單。但是Gson就沒有這種方法,如果需要解析列表,必須使用TypeToken<List<Xxx>>
,並沒有為列表設定特殊的方法,這裡依然能看到Gson希望對外的介面保持一致和簡單
,即便犧牲一點兒方便性。
泛型反序列化
為了解析泛型,Gson和Fastjson都提供了類似的機制(Gson使用TypeToken承載型別,而Fastjson使用TypeReference承載型別),利用子類繼承確定泛型型別父類的方式,獲得型別,區別是Gson的介面只接受Type型別的引數,不接受TypeToken引數,這是因為Type是JDK的自帶型別,這種設計的效果是Gson的介面非常簡單 。Fastjson的介面可以支援Type引數,也支援TypeReference引數。
小結
整體上能明顯看出來fastjson更多是長出來的,介面多而全,應該是不斷有人提需求的結果,而Gson是設計出來的,介面的一致性很強,高內聚低耦合,有些時候寧願犧牲介面的便利性,也要保證介面對外的一致性、簡單和概念完整,從設計上我是崇尚Gson的設計理念的,但實際的開發過程更容易演變成fastjson的模式,在中國程式設計師的地位真的不夠高。
參考資料
[1]. fastjson原始碼
[2]. Gson原始碼
[3].https://github.com/google/gson/blob/master/GsonDesignDocument.md