概要
Serializable はフィールドを自動的にシリアライズしてくれる反面、不要なフィールドまで含まれたり、保存形式を細かく制御できなかったりする場面があります。Externalizable は writeExternal と readExternal を自分で実装することで、どのフィールドをどの順序で書き出すかを完全に制御できるインターフェースです。内部メモやキャッシュ値など保存不要なデータを明示的に除外したい場合や、データサイズを抑えたい場合に使われます。ただし、public な引数なしコンストラクタが必須であること、read と write の順序を厳密に一致させる必要があることなど、手動ゆえの落とし穴もあります。この記事では、業務で使いそうなカタログデータのシリアライズを例に、Externalizable の実装手順と Serializable の transient との使い分けを整理します。
使いどころ
商品カタログの一時保存で、内部メモや計算済みキャッシュを除外し、必要最小限のフィールドだけをファイルに書き出す
データサイズが重要なネットワーク転送で、Serializable の自動シリアライズより小さいバイト列を生成したいとき
暗号化済みフィールドをそのままバイト列として書き出し、復元時に復号処理を挟む独自のシリアライズフローを組みたいとき
コード例
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
}
}
}Version Coverage
var でローカル変数の型推論が使える。switch 式(-> 記法)を組み合わせた分岐ロジックも簡潔に書ける。record は Externalizable 不可。
// 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 には public な引数なしコンストラクタが必須。省略するとデシリアライズ時に InvalidClassException が発生する
writeExternal と readExternal のフィールド順序が1つでもずれると、データ型の不一致で不正な値が復元される。テストで往復(round-trip)を必ず検証すること
record は全フィールドが final のため Externalizable を実装できない。record でカスタムシリアライズが必要な場面は少ないが、必要なら通常クラスに切り替える
Externalizable は Serializable のサブインターフェースだが、serialVersionUID の自動計算の挙動は同じ。互換性管理のために明示的に定義しておくのが安全
FAQ
除外したいフィールドが少数なら transient で十分です。保存対象を厳密に選びたい場合や、書き出し順序・形式を制御したい場合に Externalizable を検討してください。
Externalizable の仕様上、引数なしコンストラクタは必須です。代替として Serializable + writeObject/readObject のカスタマイズか、JSON への切り替えを検討してください。
writeExternal の先頭にバージョン番号を書き出し、readExternal でバージョンごとに読み込みロジックを分岐させるのが定石です。新フィールドはデフォルト値で補完します。