概要

Serializable はフィールドを自動的にシリアライズしてくれる反面、不要なフィールドまで含まれたり、保存形式を細かく制御できなかったりする場面があります。Externalizable は writeExternal と readExternal を自分で実装することで、どのフィールドをどの順序で書き出すかを完全に制御できるインターフェースです。内部メモやキャッシュ値など保存不要なデータを明示的に除外したい場合や、データサイズを抑えたい場合に使われます。ただし、public な引数なしコンストラクタが必須であること、read と write の順序を厳密に一致させる必要があることなど、手動ゆえの落とし穴もあります。この記事では、業務で使いそうなカタログデータのシリアライズを例に、Externalizable の実装手順と Serializable の transient との使い分けを整理します。

使いどころ

商品カタログの一時保存で、内部メモや計算済みキャッシュを除外し、必要最小限のフィールドだけをファイルに書き出す

データサイズが重要なネットワーク転送で、Serializable の自動シリアライズより小さいバイト列を生成したいとき

暗号化済みフィールドをそのままバイト列として書き出し、復元時に復号処理を挟む独自のシリアライズフローを組みたいとき

コード例

ExternalizableDemo.java
import java.io.*;

public class ExternalizableDemo {

    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;
        }

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

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

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

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

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var でローカル変数の型推論が使える。switch 式(-> 記法)を組み合わせた分岐ロジックも簡潔に書ける。record は Externalizable 不可。

Java 17
// Java 17: var + switch 式を活用
var original = new ProductCatalog(
        "P001", "Java入門書", 3800, "在庫少注意");
// switch 式で価格帯を簡潔に判定
String category = switch (original.price / 1000) {
    case 0    -> "低価格";
    case 1, 2 -> "普通";
    default   -> "高価格";
};

Library Comparison

Externalizable(標準 API)保存フィールドの選択とバイト列のサイズを厳密に制御したいとき。read/write の順序管理が手動になるため、フィールド追加時のミスが起きやすい。テストの負担が増える。
Serializable + transient除外したいフィールドが少数で、残りは自動シリアライズに任せたいとき。transient 以外の全フィールドが自動的に含まれるため、意図しないフィールドの漏出に注意が必要。
Jackson(@JsonIgnore)JSON 形式での保存・通信が前提で、特定フィールドを除外したいとき。バイナリ形式と比較してデータサイズが大きい。Java シリアライズとは互換性がない。

注意点

Externalizable には public な引数なしコンストラクタが必須。省略するとデシリアライズ時に InvalidClassException が発生する

writeExternal と readExternal のフィールド順序が1つでもずれると、データ型の不一致で不正な値が復元される。テストで往復(round-trip)を必ず検証すること

record は全フィールドが final のため Externalizable を実装できない。record でカスタムシリアライズが必要な場面は少ないが、必要なら通常クラスに切り替える

Externalizable は Serializable のサブインターフェースだが、serialVersionUID の自動計算の挙動は同じ。互換性管理のために明示的に定義しておくのが安全

FAQ

Serializable の transient と Externalizable はどう使い分けますか。

除外したいフィールドが少数なら transient で十分です。保存対象を厳密に選びたい場合や、書き出し順序・形式を制御したい場合に Externalizable を検討してください。

Externalizable で引数なしコンストラクタを作りたくない場合はどうしますか。

Externalizable の仕様上、引数なしコンストラクタは必須です。代替として Serializable + writeObject/readObject のカスタマイズか、JSON への切り替えを検討してください。

Externalizable でバージョン管理はどうすればよいですか。

writeExternal の先頭にバージョン番号を書き出し、readExternal でバージョンごとに読み込みロジックを分岐させるのが定石です。新フィールドはデフォルト値で補完します。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます