Gson 的多態性
一、簡介
在本教程中,我們將探討如何使用 Gson 管理多態性。我們還將探索一些以多態性方式處理序列化和反序列化的技術。
2. JSON中的多態性
Java 中的多態性很好理解。我們有一個類別層次結構,這樣,在適當的時候,我們可以以某些方式相同地對待不同但相關的類型。
例如,我們可能對各種 2D 形狀有一些定義。不同的形狀以不同的方式定義,但它們都有一些共同的特徵——例如,它們都可以計算面積。
因此,我們可以定義一些多型類別來定義一些形狀:
interface Shape {
double getArea();
}
class Circle implements Shape {
private final double radius;
private final double area;
Circle(double radius) {
this.radius = radius;
this.area = Math.PI * radius * radius;
}
@Override
public double getArea() {
return area;
}
}
class Square implements Shape {
private final double side;
private final double area;
Square(double side) {
this.side = side;
this.area = side * side;
}
@Override
public double getArea() {
return area;
}
}
這些形狀中的每一個都有自己的關注點,但是如果我們只關心每個形狀都是一個Shape
並且我們可以計算它的面積,那麼我們可以對它們進行相同的處理。
但這跟 JSON 有什麼關係呢?顯然,我們不能在 JSON 文件中擁有功能,但我們可以擁有重疊的數據,並且我們可能希望在 JSON 中以合理的方式表示多態性類別。
例如,上面的形狀可以表示為:
[
{
"shape": "circle",
"radius": 4,
"area": 50.26548245743669
}, {
"shape": "square",
"side": 5,
"area": 25
}
]
如果我們想將它們簡單地視為Shape
實例,那麼我們已經有了可用的area
。然而,如果我們想確切地知道它們是什麼形狀,那麼我們可以識別它們並從中提取額外的資訊。
3. 使用包裝對象
解決這個問題的最簡單方法是為每種類型使用包裝物件和不同的欄位:
class Wrapper {
private final Circle circle;
private final Square square;
Wrapper(Circle circle) {
this.circle = circle;
this.square = null;
}
Wrapper(Square square) {
this.square = square;
this.circle = null;
}
}
使用這樣的東西,我們的 JSON 將如下所示:
[
{
"circle": {
"radius": 4,
"area": 50.26548245743669
}
}, {
"square": {
"side": 5,
"area": 25
}
}
]
這在技術上不是多態的,它確實意味著 JSON 的形狀與我們之前看到的不同,但它很容易實現。特別是,我們只需要編寫我們的包裝類型,Gson 就會自動為我們做一切:
List<Wrapper> shapes = Arrays.asList(
new Wrapper(new Circle(4d)),
new Wrapper(new Square(5d))
);
Gson gson = new Gson();
String json = gson.toJson(shapes);
反序列化也完全符合我們的預期:
Gson gson = new Gson();
Type collectionType = new TypeToken<List<Wrapper>>(){}.getType();
List<Wrapper> shapes = gson.fromJson(json, collectionType);
請注意,我們需要在此處使用TypeToken
,因為我們要反序列化為通用列表。這與我們的包裝類型和多型結構無關。
然而,這確實意味著我們的Wrapper
類型需要支援所有可能的子類型。添加新的意味著需要做更多的工作才能達到我們期望的最終結果。
4. 為物件新增類型字段
如果我們只對序列化物件感興趣,我們可以簡單地向它們添加一個欄位來指示類型:
public class Square implements Shape {
private final String type = "square"; // Added field
private final double side;
private final double area;
public Square(double side) {
this.side = side;
this.area = side * side;
}
}
這樣做將導致這個新的type
欄位出現在序列化的 JSON 中,從而允許客戶端知道每個形狀的類型:
{
"type": "square",
"radius": 5,
"area": 25
}
現在這更接近我們之前看到的情況。但是,我們無法使用此技術輕鬆反序列化此 JSON ,因此只有在我們不需要這樣做的情況下它才真正可行。
5. 定制類型適配器
我們要探索的最後一種方法是編寫自訂類型適配器。這是我們可以貢獻給 Gson 實例的一些程式碼,然後該實例將為我們處理類型的序列化和反序列化。
5.1.自訂序列化器
我們想要實現的第一件事是能夠正確序列化我們的類型。這意味著使用所有標準邏輯對它們進行序列化,然後新增一個指示物件類型的附加type
欄位。
我們透過為我們的類型編寫JsonSerializer
的自訂實作來實現這一點:
public class ShapeTypeAdapter implements JsonSerializer<Shape> {
@Override
public JsonElement serialize(Shape shape, Type type, JsonSerializationContext context) {
JsonElement elem = new Gson().toJsonTree(shape);
elem.getAsJsonObject().addProperty("type", shape.getClass().getName());
return elem;
}
}
請注意,我們需要使用新的Gson
實例來序列化值本身。如果我們透過JsonSerializationContext
重複使用原始實例,那麼我們最終會陷入無限循環,序列化器不斷呼叫自身。
在這裡,我們使用完整的類別名稱作為我們的類型,但我們同樣可以使用任何我們想要支援的名稱。我們只需要某種方法來唯一地在該字串和類別名稱之間進行轉換。
使用它,生成的 JSON 將是:
[
{
"radius": 4,
"area": 50.26548245743669,
"type": "com.baeldung.gson.polymorphic.TypeAdapterUnitTest$Circle"
},
{
"side": 5,
"area": 25,
"type": "com.baeldung.gson.polymorphic.TypeAdapterUnitTest$Square"
}
]
5.2.客製化解串器
現在我們可以將類型序列化為 JSON,我們也需要能夠反序列化它們。這意味著理解type
字段,然後使用它將其反序列化為正確的類別。
我們透過為我們的類型編寫JsonDeserializer
的自訂實作來實現這一點:
public class ShapeTypeAdapter implements JsonDeserializer<Shape> {
@Override
public Shape deserialize(JsonElement json, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String typeName = jsonObject.get("type").getAsString();
try {
Class<? extends Shape> cls = (Class<? extends Shape>) Class.forName(typeName);
return new Gson().fromJson(json, cls);
} catch (ClassNotFoundException e) {
throw new JsonParseException(e);
}
}
}
我們可以透過簡單地實作兩個介面來在同一個類別中實現序列化器和反序列化器。這有助於將邏輯保持在一起,以便我們知道兩者是相互相容的。
和之前一樣,我們需要使用一個新的Gson
實例來實際進行反序列化。否則,我們將陷入無限循環。
5.3.類型適配器中的接線
現在我們已經有了一個可用於序列化和反序列化多態類型的類型適配器,我們需要能夠使用它。這意味著建立一個已連線的Gson
實例:
GsonBuilder builder = new GsonBuilder();
builder.registerTypeHierarchyAdapter(Shape.class, new ShapeTypeAdapter());
Gson gson = builder.create();
我們使用registerTypeHierarchyAdapter
呼叫來連接它,因為這意味著它將用於我們的Shape
類別以及任何實現它的東西。然後,每當該Gson
實例嘗試將實作Shape
介面的任何內容序列化為 JSON 或每當我們嘗試將 JSON 反序列化為實作Shape
介面的任何內容時,這將導致該 Gson 實例使用此轉接器:
List<Shape> shapes = List.of(new Circle(4d), new Square(5d));
String json = gson.toJson(shapes);
Type collectionType = new TypeToken<List<Shape>>(){}.getType();
List<Shape> result = gson.fromJson(json, collectionType);
assertEquals(shapes, result);
六、總結
在這裡,我們看到了一些使用 Gson 管理多態類型的技術,將它們序列化為 JSON 並將它們從 JSON 反序列化回來。
下次當您使用 JSON 和多態類型時,為什麼不嘗試其中的一些呢?
與往常一樣,本文的完整程式碼可以在 GitHub 上找到。