CodeGym /コース /JAVA 25 SELF /シリアライズの挙動を調整: カスタムメソッド

シリアライズの挙動を調整: カスタムメソッド

JAVA 25 SELF
レベル 43 , レッスン 3
使用可能

1. writeReplace と readResolve の基礎

標準のシリアライズ機能だけでは足りないことがあります。例えば、シングルトン(アプリ全体でインスタンスが1つだけのクラス)を持っていて、デシリアライズ後もその唯一性を保ちたい場合(新しいクローンを作りたくない)。あるいは、実際のオブジェクトではなく、その「軽量版」(プロキシ)をシリアライズして実装詳細を隠したり、サイズを節約したい場合です。

そのために Java には特別なメソッド writeReplacereadResolve があります。これらはシリアライズ対象やデシリアライズ直後のオブジェクトを別のオブジェクトに置き換えるためのものです。

簡単なたとえ:
友だちに荷物を送るとして、自分の代わりに箱へ「おもちゃの分身」を入れて送るイメージです。そして友だちが箱を開けたとき、手にしているのはおもちゃではなく、本物のあなたになる、という感じです(現実では起きませんが、Java では可能です)。

writeReplace

メソッド private Object writeReplace() は、シリアライズの直前に対象オブジェクトで呼ばれます。ここで任意のオブジェクトを返すと、元のオブジェクトの代わりにそのオブジェクトが実際にシリアライズされます。実装していなければ、元のオブジェクトがそのままシリアライズされます。

シグネチャ:


private Object writeReplace() throws ObjectStreamException

readResolve

メソッド private Object readResolve() は、デシリアライズ直後のオブジェクトに対して呼ばれます。ここで新しく生成されたオブジェクトを別のオブジェクトに差し替えることができます(例えばシングルトンやキャッシュ済みのインスタンスを返す)。

シグネチャ:

private Object readResolve() throws ObjectStreamException

重要:
どちらのメソッドも private で、戻り値は Object である必要があります。これは Java のシリアライズ仕様の要件です。もし public にすると、シリアライズ時に無視されます。

2. writeReplace と readResolve の実践

シングルトンと readResolve

シングルトンとは、アプリ全体でインスタンスが1つだけ存在できるクラスのことです。このオブジェクトをシリアライズして復元すると、readResolve がない場合は新しいインスタンスが作られて「唯一性」のルールが壊れます。readResolve があれば、同じインスタンスを返すことでシングルトンの考え方を保てます。

import java.io.*;

public class MySingleton implements Serializable {
    private static final MySingleton INSTANCE = new MySingleton();
    private MySingleton() {}

    public static MySingleton getInstance() {
        return INSTANCE;
    }

    // デシリアライズ後に必ず INSTANCE が返るようにする
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

解説:
readResolve がないと、デシリアライズ後は元のシングルトンと == で等しくない新しいオブジェクトになります。readResolve があれば、常に INSTANCE が返されます。

実際に確認:

MySingleton s1 = MySingleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.bin"));
out.writeObject(s1);
out.close();

ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.bin"));
MySingleton s2 = (MySingleton) in.readObject();
in.close();

System.out.println(s1 == s2); // readResolve があれば true、なければ false

writeReplace: プロキシオブジェクトをシリアライズする

オブジェクトが重すぎる、機密データを含む、あるいは完全な形で外部に出したくない場合があります。そんなときは「代用品」であるプロキシオブジェクトをシリアライズできます。

例:
たとえば User クラスにプライベートなパスワードがあるとします。パスワードはシリアライズしたくありません。

import java.io.*;

public class User implements Serializable {
    private String username;
    private transient String password; // transient: シリアライズされない

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // User の代わりに UserProxy だけをシリアライズする
    private Object writeReplace() throws ObjectStreamException {
        return new UserProxy(username);
    }

    // プロキシクラス(シリアライズ専用)
    private static class UserProxy implements Serializable {
        private String username;
        public UserProxy(String username) {
            this.username = username;
        }

        private Object readResolve() throws ObjectStreamException {
            // 実運用ではパスワードは復元できないため、空パスワードの User を返す
            return new User(username, "");
        }
    }
}

解説:

  • シリアライズ時、UserUserProxy(パスワードなし)に変換されます。
  • デシリアライズ時、UserProxyUser に戻ります(パスワードは空)。

3. イミュータブルなオブジェクト向けのカスタムシリアライズ

イミュータブル(不変)なオブジェクトはプライベートな final フィールドを使い、セッターを持たないことが多いです。標準のシリアライズでもこの制約は回避できますが、場合によっては writeReplace/readResolve を使って明示的に制御したほうがよいことがあります。

例: 値オブジェクト

import java.io.*;

public final class Money implements Serializable {
    private final int amount;
    private final String currency;

    public Money(int amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    private Object writeReplace() throws ObjectStreamException {
        return new MoneyProxy(amount, currency);
    }

    private static class MoneyProxy implements Serializable {
        private final int amount;
        private final String currency;

        MoneyProxy(int amount, String currency) {
            this.amount = amount;
            this.currency = currency;
        }

        private Object readResolve() throws ObjectStreamException {
            return new Money(amount, currency);
        }
    }
}

解説:

  • シリアライズ時、MoneyMoneyProxy(POJO)に変換されます。
  • デシリアライズ時、MoneyProxyMoney に戻ります。

writeObject/readObject との関係

writeReplace/readResolvewriteObject/readObject と独立して動作します。両方が定義されている場合、まず writeReplace が呼ばれ、その返り値のオブジェクトに対して(それが Serializable を実装していれば)writeObject が呼ばれます。

図:

flowchart LR
    A[オブジェクト] -- writeReplace --> B[プロキシオブジェクト]
    B -- writeObject --> C[バイトストリーム]
    C -- readObject --> D[プロキシオブジェクト]
    D -- readResolve --> E[最終オブジェクト]

4. 実践: オブジェクトの差し替えシリアライズ

学習用アプリにカスタムシリアライズを追加してみましょう。例えば Person クラスでは、シリアライズ時に名前だけを書き出し、年齢は無視します(プライバシーのためとします)。

ステップ 1. メインクラス

import java.io.*;

public class Person implements Serializable {
    private String name;
    private int age; // シリアライズしたくない

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private Object writeReplace() throws ObjectStreamException {
        return new PersonProxy(name);
    }

    private static class PersonProxy implements Serializable {
        private final String name;

        PersonProxy(String name) {
            this.name = name;
        }

        private Object readResolve() throws ObjectStreamException {
            return new Person(name, -1); // -1 は年齢不明を表す
        }
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

ステップ 2. テスト

public class TestCustomSerialization {
    public static void main(String[] args) throws Exception {
        Person original = new Person("Alice", 30);

        // シリアライズ
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"));
        out.writeObject(original);
        out.close();

        // デシリアライズ
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"));
        Person deserialized = (Person) in.readObject();
        in.close();

        System.out.println("シリアライズ前: " + original);
        System.out.println("デシリアライズ後: " + deserialized);
    }
}

結果:

シリアライズ前: Person{name='Alice', age=30}
デシリアライズ後: Person{name='Alice', age=-1}

ご覧のとおり、年齢はシリアライズされていません。想定どおりです。

5. 注意点とニュアンス

writeReplace/readResolve を使うべきとき

  • オブジェクト状態の一部だけをシリアライズしたいとき。
  • プロキシオブジェクトでシリアライズ/デシリアライズしたいとき.
  • Singleton パターンをサポートしたいとき。
  • 内部構造が変わりうるイミュータブルまたは複雑なオブジェクトに対して。

使わないほうがよい場合

  • transient フィールドや writeObject/readObject で足りる場合。
  • オブジェクトを別のものに置き換えるべきでない場合。

継承との互換性

スーパークラスが writeReplace/readResolve を定義していると、(オーバーライドされていない限り)サブクラスでもそれらが呼ばれます。継承階層では注意が必要です。

6. カスタムシリアライズでのよくあるミス

エラー No.1: メソッドの可視性が誤っている。 writeReplace/readResolveprivate 以外にすると、シリアライズ時に呼ばれません。private のみ!

エラー No.2: 返り値の型が一致していない。 writeReplace/readResolve の戻り値は Object でなければなりません。実際に自分の型を返す場合でも、メソッドの返り値型は Object にします。

エラー No.3: データの欠落。 プロキシオブジェクトが元のオブジェクトを復元するのに必要なデータをすべて持っていないと、一部の情報が失われます。必ず元通りに復元できることを確認してください。

エラー No.4: 不変条件の破壊。 readResolve は、プログラムの期待に合致するオブジェクトを返す必要があります(シングルトンなら INSTANCE を返すなど)。

エラー No.5: 例外を処理していない。 writeReplace/readResolveObjectStreamException をスローする可能性があります。これを処理するか、明示的にスローしてください。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION