首页 > 解决方案 > GSON LinkedHashMap 的泛型类

问题描述

几个月来我一直在研究这个解决方案,我得出的结论是没有干净的方法来实现我想要实现的目标。我觉得我在多态性方面的教育让我失望了,所以我来到 StackOverflow 寻求第二意见。对不起,如果这看起来很长而且令人费解。在过去的几个月里,这一直是我的大脑,此时我已经没有想法了。我希望有人可以看看,看看我可以通过其他方式避免所有这些混乱。

我想要实现的是两个通用类:一个可以表示任何“可保存”对象,另一个可以表示可保存对象列表(或我称之为“存储”)。可保存对象可以使用 GSON 保存自己,商店也可以使用 GSON 将自己保存到 JSON 文件中。不同之处在于可保存对象通常表示可以保存的任何 GSON 对象,而存储则从可保存对象扩展为通过 ID 成为对象的可保存哈希映射。

我正在寻找的示例输出如下:

想象一下,我有一个带有 uuid 字符串字段和名称字符串字段的对象。我希望能够为这些对象创建一个 Store,它是一个 LinkedHashMap,而且还扩展了一个 Saveable 以允许将对象保存为:

测试.json

{"dbf39199209e466ebed0061a3491ed9e":{"uuid":"dbf39199209e466ebed0061a3491ed9e","name":"Example Name"}}

我还希望能够通过 Store 的 load 方法将此 JSON 加载回对象中。

示例代码用法如下:

Store<User> users = new Store<>();
users.load();
users.add(new User("dbf39199209e466ebed0061a3491ed9e", "Example Name"));
users.save();

我的尝试

可保存的

我期望“可保存”对象能够做的事情如下:提供一个无参数的保存方法并提供一个无参数的加载方法。可保存对象表示可以通过 GSON 保存的任何对象。它包含两个字段:一个 Gson gson 对象和一个 Path 位置。我在可保存的构造函数中提供了这些。然后我想提供两个方法:一个Saveable#save()方法和一个Saveable#load()方法(或者静态Saveable#load()方法,我无所谓)。使用 Saveable 对象的方式是将它(因此它是抽象的)扩展为另一个表示某物的对象,例如 ,TestSaveable然后用法如下:

TestSaveable saveable = new TestSaveable(8);
saveable.save(); // Saves data
saveable.setData(4);
saveable = saveable.load(); // Loads old data

我还希望一个可保存的对象能够处理一个泛型,例如一个整数(想想最后一个例子,但有一个整数泛型)。这将使我能够执行我的下一个商店计划。

我的实现尝试如下:

public abstract class Saveable {

    private transient Gson gson;
    private transient Path location;

    public Saveable(Gson gson, Path location) {
        this.gson = gson;
        this.location = location;
    }

    @SuppressWarnings("unchecked")
    public <T extends Saveable> T save() throws IOException {
        if (location.getParent() != null) {
            Files.createDirectories(location.getParent());
        }
        Files.write(location, gson.toJson(this).getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, LinkOption.NOFOLLOW_LINKS);
        return (T) this;
    }

    protected <T extends Saveable> T load(Class<T> clazz, @NotNull Class<?>... generics) throws IOException {
        if (!Files.exists(location)) {
            return this.save();
        } else {
            InstanceCreator<Saveable> creator = type -> this;
            Type type = TypeToken.getParameterized(clazz, generics).getType();
            Gson newGson = gson.newBuilder().registerTypeAdapter(type, creator).create();
            return newGson.fromJson(Files.newBufferedReader(location), type);
        }
    }

}

不幸的是,这个尝试在我的目标中失败了,因为在创建我的 TestSaveable 类时,用户仍然必须通过泛型进行加载:

public class TestSaveable<T> extends Saveable {

    public boolean testBool = false;
    public T value;

    public TestSaveable(T value) {
        super(new Gson(), Path.of("test.json"));
        this.value = value;
    }

    public final TestSaveable<T> load(Class<T> generic) throws IOException {
        return super.load(TestSaveable.class, generic);
    }

}

然而,通过这个,我确实得到了一个相当干净的实现,除了几乎没有类型检查并且不断地为它添加抑制:

public class Test {

    public static void main(String[] args) {
        try {
            TestSaveable<Integer> storeB4 = new TestSaveable<>(5).save();
            storeB4.value = 10;
            TestSaveable<Integer> store = storeB4.load(Integer.class);
            System.out.println("STORE: " + store);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

专卖店

商店是可保存的扩展。存储是一个 LinkedHashMap,它可以快速轻松地将其中的所有对象保存为 GSON 中的映射。不幸的是,我什至不确定从哪里开始。我不能扩展两个对象(两个是 LinkedHashMap<String, T> 和一个 Saveable),但我也不能为 Saveable 对象使用接口。

我之前尝试使用 IStorable 和 ISaveable 类作为我在上面向您展示的抽象 Saveable 类的替代方法,但这导致我的问题的另一个非常丑陋且不可靠的解决方案。

Saveable.java

public class Saveable {

    // Suppress default constructor
    private Saveable() {}

    // Save a class to the specified location using the specified gson
    public static <T extends ISaveable> T save(T instance) throws IOException {
        Files.createDirectories(instance.getLocation().getParent());
        Files.write(instance.getLocation(), instance.getGson().toJson(instance).getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, LinkOption.NOFOLLOW_LINKS);
        return instance;
    }

    // Load a file from the specified location using the specified gson and cast it to the specified class using the specified generic
    public static <T extends ISaveable> ISaveable load(Path location, Gson gson, Class<T> clazz, Class<?> genericClazz) throws IOException {
        if (!Files.exists(location)) {
            return null;
        } else {
            TypeToken<?> type = genericClazz == null ? TypeToken.get(clazz) : TypeToken.getParameterized(clazz, genericClazz);
            ISaveable saveable = gson.fromJson(Files.newBufferedReader(location), type.getType());
            saveable.setGson(gson);
            saveable.setLocation(location);
            return saveable;
        }
    }

}

ISaveable.java

public interface ISaveable {

    // Gson
    Gson getGson();
    void setGson(Gson gson);

    // Location
    Path getLocation();
    void setLocation(Path location);

}

IStorable.java

public interface IStoreable {

    String getUuid();

}

存储.java

public class Store<T extends IStoreable> extends LinkedHashMap<String, T> implements ISaveable {

    private transient Path location;
    private transient Gson gson;

    public Store(Path location, Gson gson) {
        this.location = location;
        this.gson = gson;
    }
    public Store() {
        this.location = null;
        this.gson = null;
    }

    public Store<T> put(T value) {
        this.put(value.getUuid(), value);
        return this;
    }

    public Store<T> remove(T value) {
        this.remove(value.getUuid());
        return this;
    }

    public Store<T> save() throws IOException {
        return Saveable.save(this);
    }

    @SuppressWarnings("unchecked")
    public static <T extends IStoreable> Store<T> load(Path location, Gson gson, Class<T> genericClazz) throws IOException {
        ISaveable saveable = Saveable.load(location, gson, Store.class, genericClazz);
        if (saveable == null) {
            return new Store<T>(location, gson).save();
        } else {
            return (Store<T>) saveable;
        }
    }

}

这个解决方案几乎达到了我想要的结果,但在加载过程中很快就失败了,而且不是一个健壮的解决方案,不包括我肯定在这一点上毁掉的数百个 Java 实践:

Store<ExampleStoreable> store = Store.load(Paths.get("storetest.json"), new Gson(), ExampleStoreable.class);
store.put(new ExampleStoreable("Example Name"));
store.save();

在我收到任何评论说我不应该在 StackOverflow 上发布此内容之前:如果不在这里,还有哪里?请帮我指出正确的方向,我不想被留在黑暗中。

感谢是否有人能够提供帮助,如果没有,我理解。这无论如何都不是最简单的问题。

标签: javaarraysjsongenericsgson

解决方案


我非常接近正确的解决方案,但我的逻辑并没有对齐。

固定加载方法如下:

default <T extends ISaveable> T load() throws IOException {
    if (!Files.exists(getLocation())) {
        return save();
    } else {
        InstanceCreator<?> creator = type -> (T) this;
        Gson newGson = getGson().newBuilder().registerTypeAdapter(getType(), creator).create();
        return newGson.fromJson(Files.newBufferedReader(getLocation()), getType());
    }
}

与其试图阻止类型擦除,也不是每次调用方法时都传递类,我们只是......在构造函数中传递它。真的就是这么简单。我不关心通过构造函数发送类型,只要 .load() 和 .save() 不会导致数百行重复代码。

我不敢相信我一直如此接近解决方案。这对我来说是多么简单,这让我难以置信。猜猜这就是编程的生命,对吧?

这是完整的类,我认为它作为一个名为 ISaveable.java 的接口更好:

public interface ISaveable {

    Type getType();
    Gson getGson();
    Path getLocation();

    /**
     * Saves this object.
     *
     * @param <T> The extended object to cast to.
     * @return The object after having been saved.
     * @throws IOException Thrown if there was an exception while trying to save.
     */
    @SuppressWarnings("unchecked")
    default <T extends ISaveable> T save() throws IOException {
        Path location = getLocation().toAbsolutePath();
        if (location.getParent() != null) {
            Files.createDirectories(location.getParent());
        }
        Files.write(getLocation(), getGson().toJson(this).getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, LinkOption.NOFOLLOW_LINKS);
        return (T) this;
    }

    /**
     * Loads this object.
     *
     * @param <T> The extended object to cast to.
     * @return The object after loading the new values.
     * @throws IOException Thrown if there was an exception while trying to load.
     */
    @SuppressWarnings("unchecked")
    default <T extends ISaveable> T load() throws IOException {
        if (!Files.exists(getLocation())) {
            return save();
        } else {
            InstanceCreator<?> creator = type -> (T) this;
            Gson newGson = getGson().newBuilder().registerTypeAdapter(getType(), creator).create();
            return newGson.fromJson(Files.newBufferedReader(getLocation()), getType());
        }
    }

}

一个示例实现:

public class ExampleSaveable implements ISaveable {

    private boolean testBoolean = false;
    private String myString;

    public ExampleSaveable(String myString) {
        this.myString = myString;
    }

    @Override
    public Gson getGson() {
        return new Gson();
    }

    @Override
    public Type getType() {
        return TypeToken.get(ExampleSaveable.class).getType();
    }

    @Override
    public Path getLocation() {
        return Path.of("test.json");
    }
    
}

一个示例用法是这样的:

ExampleSaveable saveable = new ExampleSaveable("My Data!").load();
saveable.myString = "This is a replacement string!";
saveable.save();

第一次运行,输出是“我的数据!”,第二次,输出是“这是一个替换字符串!”

相应的输出 JSON 是:

{"testBoolean":false,"myString":"This is a replacement string!"}

这允许我随后扩展该类以创建我的商店。

IStorable.java

public interface IStorable {

    String getUuid();

}

存储.java

public class Store<T extends IStorable> extends LinkedHashMap<String, T> implements ISaveable {

    // GSON & Location
    private transient Gson gson;
    private transient Path location;
    private transient Type type;

    /**
     * Constructs a new store.
     *
     * @param gson The gson to use for saving and loading.
     * @param location The location of the JSON file.
     * @param generic The generic that this instance of this class is using (due to type erasure).
     */
    public Store(Gson gson, Path location, Class<T> generic) {
        this.gson = gson;
        this.location = location;
        this.type = TypeToken.getParameterized(Store.class, generic).getType();
    }

    // Putting
    public Store<T> put(T value) {
        this.put(value.getUuid(), value);
        return this;
    }
    public Store<T> putAll(T... values) {
        for (T value : values) {
            this.put(value.getUuid(), value);
        }
        return this;
    }

    // Replacing
    public Store<T> replace(T value) {
        this.replace(value.getUuid(), value);
        return this;
    }

    // Removing
    public Store<T> remove(T value) {
        this.remove(value.getUuid());
        return this;
    }

    // Implement ISaveable
    @Override
    public Gson getGson() {
        return gson;
    }
    @Override
    public Path getLocation() {
        return location;
    }
    @Override
    public Type getType() {
        return type;
    }

    // Setters
    public void setLocation(Path location) {
        this.location = location;
    }

}

推荐阅读