技術メモ

技術メモ

ラフなメモ

Javaのシリアライズ・デシリアライズを試す

Javaシリアライズ・デシリアライズについて学びます。

概要

処理の流れ

f:id:tutuz:20190519194819p:plain

https://qiita.com/Sekky0905/items/b3c6776d10f183d8fc89より引用

シリアライズ

シリアライズJavaオブジェクトをバイトのストリームに変換する処理です。

ObjectOutputStreamを用いて、プリミティブ・データ型とJavaオブジェクトのグラフをOutputStreamに書き込むことができます。このときオブジェクトが参照しているオブジェクトも含めてバイトストリームに書き込むことができます。

オブジェクトをストリームに書き込むにはwriteObjectメソッドを使います。ストリームからFileを作成する場合はFileOutputStreamを用います。

public class ObjectOutputStreamSample {
    public static void main(String[] args) throws IOException {

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("data/person.dat"));

        Person person = new Person();
        person.setName("tutuz");
        person.setAge(99);
        Set<String> hobbys = new HashSet<>();
        hobbys.add("sports");
        hobbys.add("music");
        person.setHobbys(hobbys);

        objectOutputStream.writeObject(person);
        objectOutputStream.close();
    }
}
@Data
public class Person implements Serializable {
    private String name;
    private int age;
    private Set<String> hobbys;
}

実行すると data/person.dat というファイルが作成されていることが分かります。

Serializable

シリアライズしたいオブジェクトはjava.io.Serializableインターフェースを実装する必要があります。java.io.Serializableインターフェースはマーカーインターフェースでメソッドは持ちません。そのクラスのオブジェクトがシリアライズしようとしていることを示すインターフェースです。

public class Person implements Serializable {

ObjectOutputStream

ObjectOutputStreamは、プリミティブ・データ型とJavaオブジェクトのグラフをOutputStreamに書き込みます。これらのオブジェクトを読み込む(再構築する)にはObjectInputStreamを使います。オブジェクトの持続的記憶は、そのストリームのためのファイルを使えば可能です。ストリームがネットワーク・ソケット・ストリームの場合は、ほかのホストやほかのプロセス上でオブジェクトを再構築することもできます。 ストリームに書き込めるのはjava.io.Serializableインタフェースをサポートするオブジェクトだけです。各直列化可能オブジェクトのクラスは、クラスの名前とシグネチャ、オブジェクトのフィールドと配列、および初期オブジェクトから参照されるほかのすべてのオブジェクトのクロージャを含めてコード化されます。

オブジェクトをストリームに書き込むにはwriteObjectメソッドを使います。Stringや配列を含む任意のオブジェクトがwriteObjectによって書き込まれます。複数のオブジェクトまたはプリミティブも、ストリームへの書込みが可能です。オブジェクトを読み込むときは、対応するObjectInputstreamから同じ型として、かつ書き込まれたときと同じ順序で読み込まなければいけません。

ObjectOutputStream (Java Platform SE 8)

ここではjava.io.FileOutputStreamに書き込むjava.io.ObjectOutputStreamオブジェクトを生成しています。

    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("data/person.dat"));

Objectのwrite

指定されたオブジェクトをObjectOutputStreamに書き込みます。オブジェクトのクラス、クラスのシグネチャ、クラスの非transientフィールドおよび非staticフィールドの値とそのすべてのスーパー・タイプが書き込まれます。

ObjectOutputStream#writeObjectでオブジェクトをファイルにwriteします。

    objectOutputStream.writeObject(person);
    objectOutputStream.close();

シリアライズ

シリアライズはバイトのストリームからJavaオブジェクトに変換する処理です。ここではファイルに書かれたバイトストリームからJavaオブジェクトを生成します。

public class ObjectInputStreamSample {

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("data/person.dat"));

        Person person = (Person) objectInputStream.readObject();

        objectInputStream.close();

        System.out.printf("Name   : %s\n", person.getName());
        System.out.printf("Age    : %d\n", person.getAge());
        System.out.printf("Hobbys : %s\n", person.getHobbys().toString());
    }
}

結果

Name   : tutuz
Age    : 99
Hobbys : [sports, music]

ObjectOutputStreamで生成したオブジェクトが復元できていることが分かります。

ObjectInputStream

ObjectOutputStreamを使って作成されたプリミティブ・データとプリミティブ・オブジェクトをデシリアライズします。

    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("data/person.dat"));

Objectのread

オブジェクトをストリームから読み込むにはObjectInputStream#readObjectメソッドを使います。ObjectInputStream#readObjectで取得できるのはObjectクラスのオブジェクトであるため、必要に応じてクラスのキャストが必要になります。

    Person person = (Person) objectInputStream.readObject();

serialVersionUID

オブジェクトのバージョニングという意味合いでシリアライズする際にはserialVersionUIDが関連付けられます。自分で明示的に付与することもできます。

直列化ランタイムは、各直列化可能クラスにバージョン番号serialVersionUIDを関連付けます。これは、直列化復元中に、直列化オブジェクトの送信側と受信側が、直列化に関して互換性のあるオブジェクトのクラスをロードしたかどうかを確認するために使われます。対応する送信側のクラスとは異なるserialVersionUIDを持つオブジェクトのクラスを受信側がロードした場合、直列化復元ではInvalidClassExceptionが発生します。フィールド名"serialVersionUID"を宣言することによって、直列化可能クラスは独自のserialVersionUIDを明示的に宣言できます。このフィールド名は、次のようにstatic、final、およびlong型である必要があります。

たとえば、ある時点のPersonクラスは以下の状態だったとします。

@Data
public class Person implements Serializable {
    private String name;
    private int age;
}

この状態でシリアライズしたファイルがあったとします。

またある別の時点のPersonクラスは以下のようになっていたとします。

@Data
public class Person implements Serializable {
    private String name;
    private int age;
    private Set<String> hobbys;
}

このとき古いPersonクラスのオブジェクトをデシリアライズしようとするとserialVersionUIDが異なるために以下のExceptionが発生します。serialVersionUIDはクラスのさまざまな側面に基づいて、デフォルト値を計算されます。

Exception in thread "main" java.io.InvalidClassException: io.Person; local class incompatible: stream classdesc serialVersionUID = -6416339643206023322, local class serialVersionUID = 8137427556891076672
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
    at io.ObjectInputStreamSample.main(ObjectInputStreamSample.java:13)

ただし、すべての直列化可能クラスがserialVersionUID値を明示的に宣言することを強くお薦めします。これは、デフォルトのserialVersionUIDの計算が、コンパイラの実装によって異なる可能性のあるクラスの詳細にきわめて影響を受けやすく、直列化復元中に予期しないInvalidClassExceptionが発生する可能性があるためです。

公式にもあるように、serialVersionUIDは明示的に宣言することが多いと思います。

まとめ

Javaシリアライズ・デシリアライズについて動作を確認しました。

余談ですが、Javaアプリケーション間でのオブジェクトのやりとりにはこのシリアライズ機構が用いられますが、実際にはJSONなどの別の形式でオブジェクトを保持することも多いです。この場合はJava以外のアプリケーションでオブジェクトをシリアライズ・デシリアライズすることができます。