java-recipes

ホーム シリアライズ・デシリアライズ › Ser-03

Ser-03: Externalizable によるカスタムシリアライズ

ExternalizableSerializableより細かいシリアライズ制御が可能なインターフェースです。writeExternal()readExternal()を自分で実装することで、保存するフィールドをバイト単位で完全にカスタマイズできます。

いつ使うか

  • 保存するフィールドを細かく選択・制御したいとき(transient より厳密な管理が必要な場合)
  • シリアライズのデータ形式を完全にカスタマイズしてサイズを最小化したいとき
  • シリアライズ時に値の変換(暗号化・圧縮など)を挟みたいとき

Serializable vs Externalizable の比較

特性SerializableExternalizable
実装の手間少ない(マーカーインターフェース)多い(writeExternal / readExternal を実装)
保存フィールドの制御transient で除外のみ完全なカスタマイズ可能
引数なしコンストラクタ不要public 引数なしコンストラクタが必須
record との併用可能(implements Serializable)不可(final フィールドに代入できない)

サンプルコード

ExternalizableSample.java
import java.io.*;

public class ExternalizableSample {

    // Externalizable: writeExternal/readExternal を自分で実装
    static class ProductCatalog implements Externalizable {
        private String productId;
        private String productName;
        private int price;
        private String internalNote; // 保存したくない内部メモ

        // ⚠️ Externalizable には public 引数なしコンストラクタが必須
        // デシリアライズ時にリフレクションで呼び出される
        public ProductCatalog() {}

        ProductCatalog(String productId, String productName, int price, String internalNote) {
            this.productId = productId;
            this.productName = productName;
            this.price = price;
            this.internalNote = internalNote;
        }

        // 保存するフィールドを明示的に指定
        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            out.writeUTF(productId);
            out.writeUTF(productName);
            out.writeInt(price);
            // internalNote は保存しない(意図的に除外)
        }

        // 読み込み順序は writeExternal と完全に一致させる
        @Override
        public void readExternal(ObjectInput in) throws IOException {
            this.productId = in.readUTF();
            this.productName = in.readUTF();
            this.price = in.readInt();
            this.internalNote = null; // 保存しなかったので null
        }

        @Override
        public String toString() {
            return "Product{id='" + productId + "', name='" + productName
                    + "', price=" + price + ", internalNote='" + internalNote + "'}";
        }
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ProductCatalog original = new ProductCatalog("P001", "Java入門書", 3800, "在庫少注意");
        System.out.println("元オブジェクト: " + original);

        // シリアライズ
        byte[] bytes;
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(original);
            bytes = baos.toByteArray();
        }
        System.out.println("データサイズ: " + bytes.length + " bytes");

        // デシリアライズ
        try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
             ObjectInputStream ois = new ObjectInputStream(bais)) {
            ProductCatalog loaded = (ProductCatalog) ois.readObject();
            System.out.println("復元オブジェクト: " + loaded);
            // internalNote は保存されていないので null
        }
    }
}

Externalizable では writeExternal で書いたフィールドと readExternal で読む順序を完全に一致させる必要があります。順序がずれると別のフィールドの値が混入して壊れたオブジェクトが復元されます。また public 引数なしコンストラクタが必須なのは、デシリアライズ時に JVM が最初にそのコンストラクタでオブジェクトを生成してから readExternal を呼ぶためです。

よくあるミス・注意点

writeExternal と readExternal の順序は完全一致が必須

writeExternal で書いた順序とreadExternal で読む順序が一致しないと、 別のフィールドの値が混入した壊れたオブジェクトが生成されます。 例えば productId を書いた後に productName を書いたなら、 読み込みも必ず productId → productName の順にしてください。

public 引数なしコンストラクタがないと InstantiationException になる

Externalizable を実装したクラスのデシリアライズ時、 JVM はまず引数なしコンストラクタでオブジェクトを生成し、 その後に readExternal を呼び出します。 引数なしコンストラクタが存在しないか public でない場合は 例外が発生します。必ず public ProductCatalog() {} を定義してください。

record には Externalizable を実装できない

Java 17 以降の record は すべてのコンポーネントが final のため、readExternal で代入できません。 カスタムシリアライズが必要な場合は通常クラスを使用してください。 record を使いたい場合は、全コンポーネントをシリアライズするSerializable のみ実装できます。

テストする観点

  • writeExternal で指定したフィールドが、デシリアライズ後に正しく復元されること
  • writeExternal で指定しなかったフィールド(internalNote)が、デシリアライズ後に null になること
  • public 引数なしコンストラクタを削除すると、デシリアライズ時に例外が発生すること
  • writeExternal と readExternal の読み書き順序を逆にすると、フィールド値が混入した壊れたオブジェクトが生成されること(境界値テスト)
  • シリアライズしたデータのバイトサイズが Serializable より小さいこと(不要フィールドを除外した効果の確認)

GitHub でソースコードを見る →