You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

含抽象类与多子类对象列表的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 20:57:33