【问题标题】:Gson deserialization: set final fieldGson 反序列化:设置最终字段
【发布时间】:2011-12-02 14:02:58
【问题描述】:

我使用 gson 反序列化小部件层次结构,但在反序列化最终字段时遇到问题。

例子:

public final class Screen{

    @Expose
    private final List<WidgetDefinition> children       = null;
    @Expose
    private final String name           = null;
}

public final class AWidget implements WidgetDefinition {
    @Expose
    private final String name           = null;
}

我正在使用 WidgetDefinition 的自定义反序列化器反序列化屏幕,如下所示。 Screen 中的 'name' 设置正确,AWidget 中的 'name' 保持为空。

final class Deserializer implements JsonDeserializer<WidgetDefinition> {

    public WidgetDefinition deserialize(final JsonElement json, final Type type,
                                        final JsonDeserializationContext context) {

        JsonObject jsonObject = json.getAsJsonObject();

        String typeName = jsonObject.get("type").getAsString();
        if (typeName.equals("awidget")) {
            return context.deserialize(json, AWidget.class);
        } else {
            return null;
        }
    }
}

编辑:我想知道它是否与此有关:

Gson 1.7 不会序列化集合元素中的子类字段。 2.0 添加了这个额外的信息。

(https://sites.google.com/site/gson/gson-roadmap)

【问题讨论】:

  • 为什么要反序列化为最终字段? final 的意思是声明这不会改变,所以你似乎真的在破解你自己的模型。
  • 我想要不可变的对象。除了从 gson 解析它们之外,我不会在任何地方创建这些对象,之后也无需更改它们。在我看来,不变性总是更好。
  • 是的,但是您可以将它们设为私有并且不提供设置器 - 这可以达到相同的目的吗?
  • 差不多。我在内部设置了一些成员,我喜欢通过在我的成员中设置最终部分和非最终部分来区分它们。我只是想知道为什么 gson 会这样。
  • @FabianZeindl 派对有点晚了,但我同意你的观点。当不可变对象对简化多线程模型至关重要时,Java 没有提供处理不可变对象的内部序列化能力,这对我来说是永无止境的恶化。

标签: java json deserialization gson final


【解决方案1】:

Gson 使用反射来设置 final 字段(通过.setAccessible(true)),所以你描述的问题(和其他相关问题)可能来自 Java 处理 final 的方式......

JLS 17.5.3 Subsequent Modification of final Fields

在某些情况下,例如反序列化,系统将需要在构造后更改对象的最终字段。 final 字段可以通过反射和其他依赖于实现的方式进行更改。唯一具有合理语义的模式是构造对象,然后更新对象的最终字段。在对象的 final 字段的所有更新完成之前,不应使该对象对其他线程可见,也不应读取 final 字段。 final 字段的冻结发生在设置了 final 字段的构造函数的末尾,以及通过反射或其他特殊机制每次修改 final 字段之后。

即便如此,也存在许多并发症。如果在字段声明中将最终字段初始化为编译时常量表达式(第 15.28 节),则可能不会观察到最终字段的更改,因为该最终字段的使用在编译时被常量表达式的值替换.

另一个问题是规范允许对最终字段进行积极优化。在一个线程中,允许使用未在构造函数中发生的对 final 字段的修改来重新排序对 final 字段的读取。

【讨论】:

    【解决方案2】:

    解决原始问题中的确切问题有点困难,因为没有提供完整的最少代码和示例。或许以下对 Gson 在反序列化期间如何实例化目标对象的理解会有所帮助。

    Gson 1.7.1 和 2.0 在 vanilla 反序列化操作期间都尊重最终字段分配,并且初始最终字段分配不会改变。例如:

    import com.google.gson.Gson;
    
    public class GsonFoo
    {
      public static void main(String[] args)
      {
        // {"name":"Fred","id":42}
        String json1 = "{\"name\":\"Fred\",\"id\":42}";
        System.out.println(new Gson().fromJson(json1, Bar1.class));
        // output:
        // Bar1: name=Fred, id=-1
      }
    }
    
    class Bar1
    {
      String name = "BLANK";
      final int id = -1;
    
      @Override
      public String toString()
      {
        return String.format("Bar1: name=%s, id=%d", name, id);
      }
    }
    

    另一方面,例如在反序列化过程中创建实例,因为 Gson 使用sun.misc.Unsafe——而不是用户定义的构造函数——在任何构造函数中显式定义的最终字段的赋值都不受尊重。例如:

    import com.google.gson.Gson;
    
    public class GsonFoo
    {
      public static void main(String[] args)
      {
        // {"name":"Fred","id":42}
        String json1 = "{\"name\":\"Fred\",\"id\":42}";
        System.out.println(new Gson().fromJson(json1, Bar1.class));
        // output:
        // Bar1: name=Fred, id=42
      }
    }
    
    class Bar1
    {
      String name = "BLANK";
      final int id;
    
      Bar1()
      {
        id = -1;
      }
    
      @Override
      public String toString()
      {
        return String.format("Bar1: name=%s, id=%d", name, id);
      }
    }
    

    总而言之,在原版反序列化期间,无法使用传入 JSON 中的任何数据重新分配最终字段分配,但如果最终字段分配发生在用户定义的构造函数中,这似乎是可能的。**

    **我不确定 JVM 规范是否允许某些实现宽松,以便在不同的 JVM 上运行时可能会观察到与上述不同的行为。

    【讨论】:

    • 我发现了更多行为。如果我有一个最终字段并分配给 null(无构造函数),gson 将填充该字段。如果它是原始类型或分配给其他东西,例如字符串,gson 不会触及它。
    • 确认。然后 Gson 比我想象的更不一致。
    【解决方案3】:

    问题的具体性与通用标题不太匹配,但我也遇到了同样的问题,而且这个问题对我来说是 Google 上评分最高的结果,所以尽管问题的年代久远,我还是会在这里回复。

    在这里总结其他答案的发现,似乎过去和当前版本的 Gson 反序列化对象,但 不会反序列化原语 if em> 它们已被字段初始化。

    public final class A {
        // Gson will properly deserialise foo, regardless of initialisation.
        public final String foo = "bar";
    }
    public final class B {
        // i will always be 42.
        public final int i = 42;
    }
    public final class C {
        // Gson will properly deserialise j
        public final int j;
        public C() { j = 37; }
    }
    

    这种行为中的各种不一致足以让我决定定义自定义类型适配器并省略默认构造函数。从 Gson 2.1 开始,TypeAdapter 使这变得非常简单。缺点是 Gson 现在无法自动处理这个问题。

    给定一个类型Point,定义为:

    public final class Point {
        public final int x;
        public final int y;
        public Point(final int x, final int y) {
            this.x = x;
            this.y = y;
        }
    }
    

    文档中示例的略微调整变体

    public class PointAdapter extends TypeAdapter<Point> {
      public Point read(JsonReader reader) throws IOException {
        if (reader.peek() == JsonToken.NULL) {
          reader.nextNull();
          return null;
        }
        String xy = reader.nextString();
        String[] parts = xy.split(",");
        int x = Integer.parseInt(parts[0]);
        int y = Integer.parseInt(parts[1]);
        return new Point(x, y);
      }
      public void write(JsonWriter writer, Point point) throws IOException {
        if (point == null) {
          writer.nullValue();
          return;
        }
        String xy = point.x + "," + point.y;
        writer.value(xy);
      }
    }
    

    产生预期的结果。

    【讨论】:

      【解决方案4】:

      我尝试使用 final 字段进行实验 - 首先使用初始化器设置它们,例如:

      class Test {
          final int x = 1;
          private Test() {}
      }
      

      GSON 无法正确反序列化。

      然后在构造函数中...

      class Test {
          final int x;
          private Test() {
          x = 1;
          }
      }
      

      这行得通。也许这是一个 Java 编译器优化,其中带有初始化器的最终原始或字符串类型变量被视为编译时常量,没有创建实际变量,而如果在构造函数中完成初始化,则会创建一个变量?

      【讨论】:

        猜你喜欢
        • 2021-04-22
        • 1970-01-01
        • 1970-01-01
        • 2015-08-01
        • 2012-12-25
        • 1970-01-01
        • 1970-01-01
        • 2014-02-20
        • 1970-01-01
        相关资源
        最近更新 更多