gsonwrapper、その3 Serializerの使い方と内部処理

2019年5月20日

gsonwrapperについての全投稿は/tag/gsonwrapperにあるので参照されたい。

gsonwrapper、その2の続きである。ここでは、主にSerializerの使い方を見ていき、いかに簡単に使えるかを示す。

Serializerは、シリアライズ・デシリアライズしようとする対象オブジェクトのクラスごとに用意され、以下を行う。

  • 対象のオブジェクトをJSON文字列に、あるいはそれをバイト配列化したもの、あるいはGZIP圧縮したバイト配列に変換する。
  • 上記とは逆の変換を行い、元のオブジェクトを再構成する。

当然だが、これらの一方向のみを行ってもよい。Javaオブジェクト->JSON文字列、あるいはJSON文字列->Javaオブジェクトのいずれかの変換でも可である。

Serializerの作成

クラスを指定する場合

最も単純な場合としては、変換対象とするオブジェクトのクラスを指定するだけである。これは以前の最初の例で示した。

Serializer<Foo>serializer = new Serializer<>(Foo.class);

TypeTokenを指定する場合

次の例で示したのは、変換対象とするオブジェクトのクラスがジェネリック型の場合である。この場合にはgsonで提供されているTypeTokenのサブクラスを作成しなければならない。

Serializer<List<String>> serializer = new Serializer<>(new TypeToken<List<String>>() {});

格納されるオブジェクトが実際には、字面上現れるクラスのサブクラスである場合

三番目の例として示したのは、プログラムの字面上現れるクラスではなく、実際にはそのサブクラスのオブジェクトが格納される場合である。
この場合には、SerializerコンストラクタにHandlerオブジェクトを指定しなければならない。

Handler<Foo>fooHandler = ...
Serializer<Foo>serializer = new Serializer<>(fooHandler);

Serializerの変換メソッド

Serializerの変換メソッドとして主なものを示す。

/** オブジェクトをJSON文字列に変換する */
public String serialize(T object);

/** オブジェクトをJSON文字列に変換し、さらにバイト配列化してGZIP圧縮したものを返す */
public byte[] serializeGzip(T object);

/** JSON文字列をデシリアライズして、元のオブジェクトを復帰する */
public T deserialize(String json);

/** GZIPされたバイト配列をJSON文字列に戻し、元のオブジェクトを復帰する */
public T deserializeGzip(byte[]bytes);

Handlerの意味と作成方法

Handlerの必要性

三番目のケースではHandlerという面倒なものを作成しなくてはならない。つまり、実際にJSON上に格納されるオブジェクトが、プログラムの字面上現れているクラスのサブクラスの場合である。先にも説明したことだが、JSON文字列上では「どのクラスをシリアライズしたものか」という情報が無いからである。

Handlerの機能としては、ほぼこれをサポートすることと言って差し支えない。つまり、シリアライズ時には、JSON上に格納されたデータが実際にはどのクラスであるかという情報を一緒に記録しておき、デシリアライズ時にはそれを正しく元のクラスに復帰することである。以前に示した例では以下のようになる。

{"list":[{"T":"Bar1","D":{"b":2,"a":1}},{"T":"Bar2","D":{"c":3,"a":1}}]}

“T”というフィールドにより、元のクラスがBar1、あるいはBar2であることを示しており、そのデータフィールドとしては、”D”という名前がつけられる(このフィールド名はSettingsにより任意に変更可能)。

しかし、注意して欲しいのだが、当然のことながら、他の処理系とJSON文字列をやりとりする場合には、この機能は不要だろう。おおよそJSONを扱う処理系では、一つのフィールドに「複数の型」のデータが格納されることは想定されていないと思われる。

本機能はあくまで、JSONをJavaシリアライゼーションの代わりに使いたいという向きのためのものだ。

Handlerを引数とするSerializerコンストラクタ

実は、Serializer作成の1番目と2番目の方法は、共に内部的にはHandlerを作成している。Serializerのコンストラクタのコードは、現時点で以下のようになっている。

  public Serializer(Class<T>clazz) {
    this(new HandlerBuilder<T>(clazz).build());
  }

  public Serializer(TypeToken<T>token) {
    this(new HandlerBuilder<T>(token).build());
  }

  public Serializer(Handler<T> handler) {

    // ハンドラからTypeTokenを取得する
    typeToken = handler.getTypeToken();

    // GsonBuilderを作成する
    GsonBuilder builder = new GsonBuilder();    
    if (Settings.ENABLE_COMPLEX_MAP_KEY_SERIALIZATION) {
      builder.enableComplexMapKeySerialization();
    }    
    if (Settings.SERIALIZE_NULLS) {
      builder.serializeNulls();
    }    
    if (Settings.SERIALIZE_SPECIAL_FLOATING_POINT_VALUES) {
      builder.serializeSpecialFloatingPointValues();
    }

    // このハンドラ及び複数のサブハンドラの処理をGsonBuilderに登録する。
    handler.registerToBuilder(builder);    

    // gsonを作成する
    gson = builder.create();
  }

おわかりのように、HandlerオブジェクトはGsonBuilderの働きを制御するものであり、そこから最終的にはGsonオブジェクトを作成する。

GsonBuilderのふるまい変更

先のコード中で、GsonBuilderのふるまいを変更していることがわかる。
enableComplexMapKeySerialization()、serializeNulls()、serializeSpecialFloatingPointValues()の呼び出しである。

これはSettingsにあるstatic変数を設定することにより、制御が可能だ。以下の意味になっている。

  /**
   * <p>
   * これを行わないと、マップのキーは必ずそのオブジェクトのtoString()の結果の文字列になってしまう。
   * これはなぜかというと、JSONのマップのキーは単一の文字列でなければならないからのようだ。
   * </p>
   * <p>
   * しかしこれでは、複雑なオブジェクトをキーとして使っている場合には、適切なtoString()を定義しないと
   * いけないし(復帰方法は調べていない)、逆にenumにtoString()が定義されていると、適切なJSON化が
   * できなくなる。
   * ここでは、JSON文字列としての不適切さよりも、Javaオブジェクトの直列化・復帰を主眼にしているので、
   * このオプションを指定する。
   * </p>
   */
  public static boolean ENABLE_COMPLEX_MAP_KEY_SERIALIZATION = true;

  /**
   * <p>
   * これを行わないと、値がnullのフィールドはフィールド自体が省略されてしまう。
   * 以下のケースのaフィールドはこれでも問題が無いが、bのハッシュマップの値がnullの場合には、キーも
   * 格納されなくなってしまう。
   * </p>
   * <pre>
   * public static class Sample {
   *   String a;
   *   HashMap<Integer, String>b = new HashMap<Integer, String>();
   * }
   * </pre>
   * <p>
   * 例えば、以下のようなケースの場合、ハッシュマップの値がnullなのでキー自体も現れなくなってしまう。
   * </p>
   * <pre>
   * Sample sample = new Sample();
   * sample.b.put(123, null);
   * </pre>
   */
  public static boolean SERIALIZE_NULLS = true;

  /**
   * <p>
   * JSON自体の仕様ではNaNやInfiniteは存在しないが、これが無いと値が落ちてしまうためサポートする。
   * </p>
   */
  public static boolean SERIALIZE_SPECIAL_FLOATING_POINT_VALUES = true;

gsonオブジェクトの利用

gsonオブジェクトを作成した後は、Serializerでは基本的にそのメソッドを呼び出すだけになる。

シリアライズの場合には、

  public String serialize(T object) {
    if (object == null) return null;
    try {
      return gson.toJson(object, typeToken.getType());
    } catch (RuntimeException ex) {
      throw new JsonException(ex);
    }
  }

デシリアライズの場合には、

  public T deserialize(String json) {
    if (json == null) return null;
    try {
      return (T)gson.fromJson(json, typeToken.getType());
    } catch (JsonClassNotFoundException ex) {
      // 復帰時にクラスが見つからない場合
      if (nullIfClassNotFound) return null;          
      throw ex;
    } catch (JsonException ex) {
      // 上記以外のJSON例外
      throw ex;
    } catch (RuntimeException ex) {
      // 上記以外のランタイム例外
      throw new JsonException(ex);
    }
  }

となっている。