简体   繁体   中英

Generic Class for a GSON LinkedHashMap

I have been working on this solution for months and I have come to the conclusion that there is no clean way to achieve what I am trying to achieve. I feel as though my education in polymorphism is failing me, so I've come to StackOverflow to get a second opinion. Sorry if this seems long and convoluted. That's been my brain for the past couple of months and at this point I'm out of ideas. I'm hoping somebody can take a look and see that I could've avoided all this mess by doing it some other way.

What I am trying to achieve is two generic classes: One that can represent any "saveable" object, and one that can represent a list of saveable objects (or what I call a "store"). A saveable object can save itself using GSON, and a store can also save itself using GSON to a JSON file. The difference being that saveable objects are generically representing any GSON object that can be saved, whereas stores are extending from saveables to become a saveable hash map of objects via IDs.

An example output I am looking for is as so:

Imagine I have an object with a uuid string field and a name string field. I want to be able to create a Store, which is a LinkedHashMap, of these objects, but also extend a Saveable to allow the objects to be saved as so:

test.json

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

I would also like to be able to load this JSON back into the objects via the Store's load method.

An example code usage would be like so:

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

My Attempts

Saveables

What I expect a "saveable" object to be able to do is as follows: provide a non-argumented method for saving and provide a non-argumented method for loading. A saveable object represents any object that can be saved via GSON. It contains two fields: a Gson gson object and a Path location. I provide those in the constructor of my saveable. I then want to provide two methods: a Saveable#save() method and a Saveable#load() method (or a static Saveable#load() method, I am indifferent). The way you use a Saveable object is by extending it (so it is abstract) to another object representing something, say, TestSaveable , and then the usage is as so:

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

I also would like a saveable object to be able to handle a generic, such as an integer (think of the last example but with an integer generic). This would allow me to execute my next plan for Stores.

My attempt at an implementation was the following:

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);
        }
    }

}

Unfortunately, this attempt failed in my goal, because upon making my TestSaveable class users still had to pass the generic through for loading:

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);
    }

}

However, through this I did get a fairly clean implementation with the exception of little to no type checking at all and constantly having to add supressions for it:

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();
        }
    }

}

Stores

Stores are an extension of saveables. A store is a LinkedHashMap which will quickly and easily save all of the objects in it as a map in GSON. Unfortunately, I'm not even sure where to start on this. I cannot extend two objects (the two being a LinkedHashMap<String, T> and a Saveable), but I also cannot use interfaces for the Saveable object.

I previously tried the following using the IStorable and ISaveable classes as an alternative to the abstract Saveable class I've shown you above, but this resulted in another very ugly and non-robust solution to my issue.

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();

}

Store.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;
        }
    }

}

This solution achieved me almost the result I was looking for, but fell short quickly on the loading process as well as just not being a robust solution, excluding the hundreds of Java practices I'm sure to have ruined at this point:

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

And before I get any comments saying I shouldn't be posting this on StackOverflow: if not here, where else? Please help point me in the right direction, I'd love to not be left in the dark.

Thanks if anyone is able to help and if not I understand. This isn't the easiest question by any means.

I was extremely close to the correct solution, but my logic just wasn't lining up.

The fixed load method is as follows:

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());
    }
}

Instead of attempting to prevent type erasure, and instead of passing the class every time we call the method, we just... pass it in the constructor. It was really that simple. I don't care about sending the type through the constructor, as long as.load() and.save() do not result in hundreds of lines of repetitive code.

I can't believe I was this close to the solution the whole time. It's incredible to me how simple this was. Guess that's the life of programming, right?

Here is the full class, which I determined was better as an interface called 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());
        }
    }

}

An example implementation:

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");
    }
    
}

And an example usage is like so:

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

On the first run, the output is "My Data,", on the second, the output is "This is a replacement string!"

The corresponding output JSON was:

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

This allowed me to subsequently extend the class to create my Store.

IStorable.java

public interface IStorable {

    String getUuid();

}

Store.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;
    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM