読者です 読者をやめる 読者になる 読者になる

【Java】Serializableの基本(シリアライズ・直列化)

スポンサーリンク

Serializable の基本的な事についてまとめました。

Serializableとは

Serializableは日本語で「直列化」といいます。オブジェクトを出力ストリームに書き出すことをシリアライズまたは直列化と呼びます。また、シリアライズされたオブジェクトを読み込んで、メモリ上に復元することをデシリアライズまたは直列化復元と呼びます。 通常、オブジェクトはそのままストリームに書き出して読み込むことが出来ないので、読み書きできる形にデータを整形する事をシリアライズ(直列化)と呼んでいるようです。

オブジェクトをシリアライズ可能にするにはjava.io.Serializableインタフェースを実装する必要があります。ただし、メソッドや定数をもたないインタフェースであるため、オーバーライドするメソッドはありません。

また、シリアライズの出力を行うObjectOutputStream#writeObjectで「シリアライズ可能かどうか」を調べる為に、if(obj instanceof Serializable)という判定がありますので、もし Serializable インタフェースを実装してないオブジェクトをシリアライズしようとするとNotSerializableExceptionが発生します。

という訳で Serializable を実装するだけで全てシリアライズ可能になるわけではなく、このクラスはシリアライズ可能ですと宣言するようなものになります。

Serializableを使用したオブジェクトの読み書き

シリアライズしたいクラスPerson.javaSerializableを実装します。

ObjectOutputStreamを使用して Person オブジェクトをストリーム(ファイル)に書き出し、ObjectInputStreamで保存したオブジェクトをストリーム(ファイル)から読み込みます。

シリアライズの対象

オブジェクトのシリアライズは、オブジェクトが保持する固有データ、つまりインスタンス変数がシリアライズ対象のデータとなります。static 変数はシリアライズ対象外です。また、明示的にシリアライズ対象外にしたい場合はtransient修飾子を付けます。

private static final String hoge = "abc";  // 対象外
private String firstName;  // 対象
private String lastName;  // 対象
private transient int age;  // 対象外

シリアライズの継承

クラスのオブジェクトをシリアライズする場合、そのクラスが Serializable インタフェースを実装していなくても、スーパークラスが Serializable インタフェースを実装していればシリアライズ可能です。

public class Employee implements Serializable {
...
}

public class Regular extends Employee {
...
}

デシリアライズ時のコンストラクタの挙動ついて

今までの例ではObjectInputStream#readObjectでデシリアライズする場合、ファイルからデータを読み込んでオブジェクトの復元をおこなっているため、コンストラクタが呼び出されません。

ただし、スーパークラスが Serializable インタフェースを実装していなくて、サブクラスが Serializable インタフェースを実装している場合、デシリアライズの際にスーパークラスのコンストラクタが呼び出されてインスタンス化されます。

// スーパークラス
public class Employee {
    public Employee() {
        System.out.println("Employeeコンストラクタ");
    }
}
// サブクラス
public class Regular extends Employee implements Serializable {
    public Regular() {
        System.out.println("Reqularコンストラクタ");
    }
}
...
@Test
public class SerializableSampleTest {
    public void serializableConstructor() {
        // シリアライズ
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("regular.txt"))) {
            Employee regular = new Regular();
            oos.writeObject(regular);
            System.out.println("シリアライズ完了");
        } catch (IOException e) {
            fail(e.getMessage());
        }

        // デシリアライズ
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("regular.txt"))) {
            Employee deRegular = (Employee) ois.readObject();
            System.out.println("デシリアライズ完了");
        } catch (IOException | ClassNotFoundException e) {
            fail(e.getMessage());
        }
    }

実行結果です。

Employeeコンストラクタ
Reqularコンストラクタ
シリアライズ完了
Employeeコンストラクタ
デシリアライズ完了

serialVersionUID

一度は見た事のあるシリアルバージョンUID(serialVersionUID)ですが、Eclipse で警告が出たからとりあえず付けてたという人も多いはず。これはオブジェクトの書き出し・読み込み前後でクラスのバージョンが異なっていないかを識別する為のものです。なので、クラスのメンバに変更があればこの数値も変更する必要があります。

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
}

JavaDoc を見ると以下のように記述されており、定義されてない場合は自動で生成されるようですが、明示的に定義する方がいいようです。

直列化可能クラスがserialVersionUIDを明示的に宣言しない場合、直列化ランタイムは「Java(TM)オブジェクト直列化仕様」で説明されているように、クラスのさまざまな側面に基づいて、クラスのserialVersionUIDのデフォルト値を計算します。ただし、すべての直列化可能クラスがserialVersionUID値を明示的に宣言することを強くお薦めします。
Serializable (Java Platform SE 8)