【问题标题】:What java class should I use for pair objects if I want to read them in from a json file with gson?如果我想用 gson 从 json 文件中读取它们,我应该为对对象使用什么 java 类?
【发布时间】:2021-05-02 13:12:57
【问题描述】:

我正在用 gson(在 Java 中)编写 json 文件,但我也想将它们读回。而且,我有一些表示键值对的数据(其中键仅在运行时知道),所以我想要要使用 pair 类来表示它们,我目前正在使用 org.apache.commons.lang3.tuple 中的 pair 类,但它没有像 gson 这样的 noArg 构造函数需要重新读取数据,因此代码当我执行该步骤时失败。是否有其他课程会更好?这是我想要的代码的小版本。

import org.apache.commons.lang3.tuple.Pair;
import com.google.gson.GsonBuilder;
import com.google.gson.GsonBuilder;

class JsonFile { ArrayList<Event> events; 
  Public void diff(JsonFile other) {
    // code not shown, but checks some things for being equal
  } 
}

class Event { String type; // of event
  Parameters params;
  Public Event(String type, Parameters params) { 
    this.type = type;
    this.params = params;
  }
}

class Parameters extends ArrayList<Pair<String, Object>> {}

JsonFile eventsToWrite; // populated by code
JsonFile eventstoDiff; // a stored copy of what the file "should" look like

// ...
// Some code filling eventsToWrite
   Parameters params = new Parameters();
   params.add(Pair.of("xyzzy", "is a magic name"));
   params.add(Pair.of("foo", "bar");
   Event event = new Event("lispy event", params);
   eventsToWrite.add(event);
// ...

Reader reader new BufferedReader(new FileReader(new File("toDiff.json")));
GsonBuilder bld = new GsonBuilder();
Gson gsonRead = bld.create();
eventsToDiff = gsonRead.fromJson(reader, JsonFile.class); // this fails
eventsToWrite.diff(eventsToDiff);

【问题讨论】:

  • 更糟糕的是,当 gson 写出对数组时,它只是写 {},因为对没有内容。

标签: java json gson key-value


【解决方案1】:

我看到了几个选项:

  1. 您可以使用MutablePair 代替Pair。它应该开箱即用。
  2. 您可以使用Map&lt;String,Object&gt; 而不是ArrayList&lt;Pair&lt;String, Object&gt;&gt;。此解决方案仅在参数键唯一时有效。
  3. 您可以创建自己的Parameter 类。
  4. 如果您坚持使用 Pair 并且希望保持它不可变(实际上这是有道理的),则需要实现自己的 TypeAdapter
  5. 最后但同样重要的是:使用 Jackson 而不是 Gson。

【讨论】:

    【解决方案2】:

    TL;DR:Gson 不能使用 Pair,因为没有办法实例化它(至少 Gson 知道:除非你告诉 Gson 使用 Pair.of)。

    很抱歉,您的解决方案存在一些问题。

    • 不要在可以使用List的声明站点绑定ArrayList
    • ArrayList 扩展Parameters 是一个相当糟糕的选择(ArrayList 有那么特别吗?如果你将来想要LinkedList 怎么办?如果你需要一个Set 怎么办?哪个设置?)​​ - - 更喜欢聚合和封装集合。
    • Pair 可以替换为Map.Entry -- 尽可能使用最通用的类​​型(类似于上一项)。
      • 令人惊讶的是,对此类对/条目使用List可能比使用Map 更好(如果需要以多映射方式存储多个键怎么办?)
    • 不幸的是,Gson 不会让您轻松序列化/反序列化 Object,因为 无法从 JSON 中重构原始对象(Gson 应该如何知道要反序列化的类,而有数万个类?哪一个更可取?为什么要为启发式方法或将类型标签直接​​烘焙到 JSON 中以让 Gson 成为原始对象类(如果可能,包括参数化)?)——在 @ 上查看更多关于类似问题的 cmets 987654321@.

    使其与 Gson 一起工作的一种可能方法是迁移到字符串-字符串对(以便 Gson 知道 如何 为键和值进行 JSON 往返)并编写一对- 感知类型适配器工厂。

    final class PairTypeAdapterFactory
            implements TypeAdapterFactory {
    
        private static final TypeAdapterFactory instance = new PairTypeAdapterFactory();
    
        private PairTypeAdapterFactory() {
        }
    
        static TypeAdapterFactory getInstance() {
            return instance;
        }
    
        @Override
        @Nullable
        public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
            if ( !Pair.class.isAssignableFrom(typeToken.getRawType()) ) {
                return null;
            }
            final ParameterizedType parameterizedType = (ParameterizedType) typeToken.getType();
            final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            final TypeAdapter<Pair<Object, Object>> pairTypeAdapter = resolveTypeAdapter(gson, actualTypeArguments[0], actualTypeArguments[1]);
            @SuppressWarnings("unchecked")
            final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) pairTypeAdapter;
            return typeAdapter;
        }
    
        private <L, R> TypeAdapter<Pair<L, R>> resolveTypeAdapter(final Gson gson, final Type leftType, final Type rightType) {
            @SuppressWarnings("unchecked")
            final TypeAdapter<L> leftTypeAdapter = (TypeAdapter<L>) gson.getDelegateAdapter(this, TypeToken.get(leftType));
            @SuppressWarnings("unchecked")
            final TypeAdapter<R> rightTypeAdapter = (TypeAdapter<R>) gson.getDelegateAdapter(this, TypeToken.get(rightType));
            return new TypeAdapter<Pair<L, R>>() {
                @Override
                public void write(final JsonWriter out, final Pair<L, R> value)
                        throws IOException {
                    out.beginArray();
                    leftTypeAdapter.write(out, value.getLeft());
                    rightTypeAdapter.write(out, value.getRight());
                    out.endArray();
                }
    
                @Override
                public Pair<L, R> read(final JsonReader in)
                        throws IOException {
                    in.beginArray();
                    final L left = leftTypeAdapter.read(in);
                    final R right = rightTypeAdapter.read(in);
                    in.endArray();
                    return Pair.of(left, right);
                }
            }
                    .nullSafe();
        }
    
    }
    

    上面的代码非常简单,它还使用数组来打包对,因为您可能想要存储重复的键并节省一些空间(此外,二元素数组很可能是对的良好载体)。

    @NoArgsConstructor
    @AllArgsConstructor
    @EqualsAndHashCode
    @ToString
    final class Event {
    
        String type;
        List<Pair<String, String>> params;
    
    }
    
    @NoArgsConstructor
    @AllArgsConstructor
    @EqualsAndHashCode
    @ToString
    final class JsonFile {
    
        List<Event> events;
    
    }
    
    private static final Gson gson = new GsonBuilder()
            .disableHtmlEscaping()
            .disableInnerClassSerialization()
            .registerTypeAdapterFactory(PairTypeAdapterFactory.getInstance())
            .create();
    
    @Test
    public void test() {
        final JsonFile before = new JsonFile();
        final List<Pair<String, String>> params = new ArrayList<>();
        params.add(Pair.of("xyzzy", "is a magic name"));
        params.add(Pair.of("foo", "bar"));
        final Event event = new Event("lispy event", params);
        before.events = new ArrayList<>();
        before.events.add(event);
        final String json = gson.toJson(before, JsonFile.class);
        final JsonFile after = gson.fromJson(json, JsonFile.class);
        Assertions.assertEquals(before, after);
    }
    

    中间JSON如下:

    {
      "events": [
        {
          "type": "lispy event",
          "params": [
            [
              "xyzzy",
              "is a magic name"
            ],
            [
              "foo",
              "bar"
            ]
          ]
        }
      ]
    }
    

    另一个使用 Gson 的(有点疯狂的)想法是为Object 实现一个启发式类型适配器,并使用自定义类型使用注释进行注释,例如 @Heuristics(这些类型的注释随 Java 8 一起提供)。这将允许您像这样参数化对:Pair&lt;String, @Heuristics Object&gt;,但据我所知,Gson 无法在类型标记中携带类型使用注释,因此类型适配器工厂无法获取它们并应用自定义类型适配器(注意:为Object 实现这样的类型适配器,即使是那些没有注释的,也可能会导致不需要的全局效果。


    根据您对此的新信息,我可能是这样实现的(没有太多评论,但如果有更多其他要求,可以重新实现):

    @NoArgsConstructor
    @AllArgsConstructor
    @EqualsAndHashCode
    @ToString
    final class JsonFile {
    
        List<Event<?>> events;
    
    }
    
    @NoArgsConstructor
    @AllArgsConstructor
    @EqualsAndHashCode
    @ToString
    final class Event<T> {
    
        String type;
        Map<String, T> params;
    
    }
    
    final class EventTypeAdapterFactory
            implements TypeAdapterFactory {
    
        private final Map<String, Class<?>> typeMap;
    
        private EventTypeAdapterFactory(final Map<String, Class<?>> typeMap) {
            this.typeMap = typeMap;
        }
    
        static TypeAdapterFactory getInstance(final Map<String, Class<?>> typeMap) {
            return new EventTypeAdapterFactory(typeMap);
        }
    
        @Override
        @Nullable
        public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
            if ( !Event.class.isAssignableFrom(typeToken.getRawType()) ) {
                return null;
            }
            final Map<String, TypeAdapter<?>> typeAdapterMap = typeMap.entrySet()
                    .stream()
                    .collect(Collectors.toMap(Map.Entry::getKey, e -> gson.getDelegateAdapter(this, TypeToken.getParameterized(Map.class, String.class, e.getValue()))));
            final TypeAdapter<Event<?>> typeAdapter = new TypeAdapter<Event<?>>() {
                @Override
                public void write(final JsonWriter out, final Event<?> value)
                        throws IOException {
                    out.beginObject();
                    out.name("type");
                    out.value(value.type);
                    out.name("params");
                    @Nullable
                    @SuppressWarnings("unchecked")
                    final TypeAdapter<Object> paramsTypeAdapter = (TypeAdapter<Object>) typeAdapterMap.get(value.type);
                    if ( paramsTypeAdapter == null ) {
                        throw new IllegalStateException(value.type + " is not mapped to a specific type");
                    }
                    paramsTypeAdapter.write(out, value.params);
                    out.endObject();
                }
    
                @Override
                public Event<?> read(final JsonReader in) {
                    final JsonObject root = Streams.parse(in)
                            .getAsJsonObject();
                    final String type = root.getAsJsonPrimitive("type")
                            .getAsString();
                    @Nullable
                    @SuppressWarnings("unchecked")
                    final TypeAdapter<Object> paramsTypeAdapter = (TypeAdapter<Object>) typeAdapterMap.get(type);
                    if ( paramsTypeAdapter == null ) {
                        throw new IllegalStateException(type + " is not mapped to a specific type");
                    }
                    final JsonObject rawParams = root.getAsJsonObject("params");
                    @SuppressWarnings("unchecked")
                    final Map<String, ?> params = (Map<String, ?>) paramsTypeAdapter.fromJsonTree(rawParams);
                    return new Event<>(type, params);
                }
            }
                    .nullSafe();
            @SuppressWarnings("unchecked")
            final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) typeAdapter;
            return castTypeAdapter;
        }
    
    }
    
    public final class EventTypeAdapterFactoryTest {
    
        private static final Gson gson = new GsonBuilder()
                .disableHtmlEscaping()
                .disableInnerClassSerialization()
                .registerTypeAdapterFactory(EventTypeAdapterFactory.getInstance(ImmutableMap.of("lispy event", String.class)))
                .create();
    
        @Test
        public void test() {
            final JsonFile before = new JsonFile();
            final Map<String, Object> params = new LinkedHashMap<>();
            params.put("xyzzy", "is a magic name");
            params.put("foo", "bar");
            final Event<Object> event = new Event<>("lispy event", params);
            before.events = new ArrayList<>();
            before.events.add(event);
            final String json = gson.toJson(before, JsonFile.class);
            System.out.println(json);
            final JsonFile after = gson.fromJson(json, JsonFile.class);
            Assertions.assertEquals(before, after);
        }
    
    }
    
    {
      "events": [
        {
          "type": "lispy event",
          "params": {
            "xyzzy": "is a magic name",
            "foo": "bar"
          }
        }
      ]
    }
    

    【讨论】:

    • 感谢详细的 cmets 和指针。我仍在吸收它们。我使用 ArrayList 而不是列表,因为它为我提供了一个 Iterator,我在未显示的代码中需要它。
    • 同样需要 Object.参数实际上是一个标记的联合。 “类型”字段是标签,但第二个对象可以是多个类。我切换之前的前一个解决方案对我需要携带的每种对象类型使用不同的变量。但是我的 json 消费者希望将其视为 ``` "params": [ "xyzzy": "is a magic name", "foo": "bar" ] ```
    • 1) [ "xyzzy": "is a magic name", "foo": "bar" ] 不是 JSON。 2)你没有提到type字段的作用来命名相关的标记联合。当然,Gson 并非无法做到这一点。又是这样,目前尚不清楚为什么单个类型字段应该同时覆盖所有第二类型字段。我猜type 字段更好地应该是该对的一部分,使其成为三元组,以便..., Object&gt; 可以正确处理标记的联合。您必须有一个类型适配器(序列化器/反序列化器对)让 Gson 了解这些细节。拥有Object 是没用的。
    • @intel_chris 查看评论和答案中的更新。
    • 是的,很抱歉我的问题不清楚。甚至称其为标记工会也有些牵强。它们是各种各样的东西,有时是动态类型语言中的变量名称,有时是命令选项的名称等。但它们确实是列表中的有序项,有时类型名称重复,第二个参数肯定是并不总是一个字符串,我的代码并不关心,因为除了通过 Json“报告它”之外,我什么都不做。我只是想让我的 Json 看起来像我的客户想要的那样。数组中的名称、值对......
    【解决方案3】:

    如果您检查Pair source code,您会看到它是一个没有字段但有一些getter 和setter 的抽象类。 GSON 需要字段(并且无法实例化抽象类或接口,因此在反序列化时会失败)。 See this for example.

    现在Mafor's answer 建议作为第一个选项 -MutablePair - 是这个abstract class Pair 的实现。它有供 Gson 使用的字段。

    public L left;
    public R right;
    

    这是一个可行的选择。

    Jackson 可以使用 getter 和 setter。但是,如果您将类型声明为 Pair 而不是 MutablePair 则没有设置器(也没有字段),那么 反序列化 可能仍然会失败。因此,仅杰克逊可能不是答案。如果你使用MutablePair,Gson 也可以工作。

    我认为你应该考虑创建自己的MyPair 类,而不是试图找到一些现有的“标准”

    @Getter
    @Setter
    public class MyPair {
        private String left;
        private Object right;
    }
    

    或者重构您的设计以在没有冲突键或出现冲突键时使用Map,例如,使用接口MultiValuedMap的一些合适实现

    【讨论】:

    • 这很有用。我没有尝试 mutable pair,因为我的客户建议我将每个参数都设为 map,这可能比 mutablepair 开销更大,但我得到了客户想要的输出 Json,这是我真正关心的,因为我不知道客户使用什么来阅读它,以及他们为什么想要他们想要的东西(除了给他们想要的东西之外,没有理由关心)。
    【解决方案4】:

    我最终得到的结果(在与我的客户讨论后)基本上是这样的:

    @NoArgsConstructor
    @AllArgsConstructor
    @EqualsAndHashCode
    final class Event {
        String type;
        List<Map<String, Object>> params;  
    }
    

    该地图只映射一件事,因此根据@Mafor 的建议 MutablePair 可能会更好,但我的客户要求使用 Map,即使它每张地图只有一对并且过度杀伤。

    另外,正如 @fluffy 所讨论的,我使用 ArrayList 来获取 diff 中代码的 .iterator() 成员函数,它按顺序比较参数(相同的参数以不同的顺序表示其他内容(例如复制 AB和文案BA不一样)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-04-29
      • 1970-01-01
      • 1970-01-01
      • 2020-08-11
      • 1970-01-01
      • 1970-01-01
      • 2015-01-07
      • 1970-01-01
      相关资源
      最近更新 更多