含抽象类与多子类对象列表的GSON序列化反序列化问题
嘿,这个问题我太熟了——GSON对抽象类/接口的反序列化确实天生不友好,因为它不知道该把JSON映射到哪个具体子类,而你又不能丢了多态的draw()行为对吧?下面给你几个生产环境里常用的解决方案,各有优劣,你可以根据自己的场景选:
方案1:手动实现TypeAdapter,完全控制序列化逻辑
核心思路是序列化时把子类的类型标识写入JSON,反序列化时根据这个标识实例化对应的子类,完全由你掌控字段的读写逻辑。
首先写一个针对ScoreItem的TypeAdapter:
import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.awt.Color; public class ScoreItemTypeAdapter extends TypeAdapter<ScoreItem> { @Override public void write(JsonWriter out, ScoreItem value) throws IOException { out.beginObject(); // 写入子类类型标识,比如类名 out.name("itemType").value(value.getClass().getSimpleName()); // 写入ScoreItem的公共字段 out.name("score").value(value.score); out.name("colorRgb").value(value.color.getRGB()); // 其他公共字段依次写入... out.endObject(); } @Override public ScoreItem read(JsonReader in) throws IOException { in.beginObject(); String itemType = null; int score = 0; Color color = null; // 读取JSON中的字段 while (in.hasNext()) { switch (in.nextName()) { case "itemType": itemType = in.nextString(); break; case "score": score = in.nextInt(); break; case "colorRgb": color = new Color(in.nextInt()); break; // 处理其他公共字段 } } in.endObject(); // 根据类型标识创建对应子类实例 ScoreItem item = switch (itemType) { case "RadarItem" -> new RadarItem(); case "LineItem" -> new LineItem(); default -> throw new IllegalArgumentException("Unknown ScoreItem type: " + itemType); }; // 给实例赋值公共字段 item.score = score; item.color = color; // 其他字段赋值... return item; } }
然后创建Gson实例时注册这个适配器:
Gson gson = new GsonBuilder() .registerTypeAdapter(ScoreItem.class, new ScoreItemTypeAdapter()) .create();
优点:完全可控,不需要额外依赖;缺点:如果ScoreItem字段很多,手动写读写逻辑会比较繁琐。
方案2:用GSON官方的RuntimeTypeAdapterFactory(推荐)
GSON的扩展库gson-extras提供了RuntimeTypeAdapterFactory,可以自动帮你处理多态的序列化/反序列化,不用手动写适配器,非常省心。
首先添加依赖(以Maven为例,Gradle类似):
<dependency> <groupId>com.google.gson</groupId> <artifactId>gson-extras</artifactId> <version>2.10.1</version> <!-- 用最新稳定版 --> </dependency>
然后配置Gson:
import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.runtimeTypeAdapterFactory.RuntimeTypeAdapterFactory; // 创建类型适配器工厂,指定父类和类型字段名(比如"type") RuntimeTypeAdapterFactory<ScoreItem> typeFactory = RuntimeTypeAdapterFactory.of(ScoreItem.class, "type") .registerSubtype(RadarItem.class, "radar") // 可以自定义类型标识,不一定用类名 .registerSubtype(LineItem.class, "line"); Gson gson = new GsonBuilder() .registerTypeAdapterFactory(typeFactory) .create();
这样序列化时,GSON会自动把type字段写入JSON(值就是你注册的"radar"/"line"),反序列化时根据这个字段自动实例化对应的子类,完美保留多态的draw()行为。
优点:代码简洁,自动处理字段,适合子类较多的场景;缺点:需要引入额外的gson-extras依赖。
方案3:用组合模式替换继承,彻底避免抽象类序列化问题
如果不想和GSON的序列化规则较劲,可以重构代码结构,把绘制逻辑从ScoreItem的子类中抽离出来,用组合模式代替继承——让ScoreItem变成普通类,持有一个绘制策略的实例,这样序列化就不会有抽象类的问题了。
首先定义绘制策略接口:
public interface DrawStrategy { void draw(ScoreItem item); } // 雷达绘制策略 public class RadarDrawStrategy implements DrawStrategy { @Override public void draw(ScoreItem item) { // 原来RadarItem的draw()逻辑,这里可以直接用item的score、color等字段 // RADAR IMPLEMENTATION ... } } // 折线绘制策略 public class LineDrawStrategy implements DrawStrategy { @Override public void draw(ScoreItem item) { // 原来LineItem的draw()逻辑 // LINE IMPLEMENTATION ... } }
然后修改ScoreItem为普通类:
import java.awt.Color; public class ScoreItem { public int score; public Color color; public String drawStrategyType; // 存储策略类型标识,比如"radar"/"line" private transient DrawStrategy drawStrategy; // transient标记不序列化,反序列化后初始化 public void draw() { if (drawStrategy == null) { initDrawStrategy(); // 懒加载策略 } drawStrategy.draw(this); } // 初始化绘制策略的方法 private void initDrawStrategy() { this.drawStrategy = switch (drawStrategyType) { case "radar" -> new RadarDrawStrategy(); case "line" -> new LineDrawStrategy(); default -> throw new IllegalArgumentException("Unknown draw strategy: " + drawStrategyType); }; } }
最后在Graphic类中,反序列化后直接调用draw()即可(initDrawStrategy会懒加载):
public void generate() { List<ScoreItem> scoreItems = ...; // 反序列化后的列表 for (ScoreItem scoreItem : scoreItems) { scoreItem.draw(); } }
优点:重构后代码结构更灵活,绘制逻辑和数据分离,符合"组合优先于继承"的设计原则;缺点:需要修改现有代码的结构,适合有重构空间的场景。
内容的提问来源于stack exchange,提问作者reynard80

