概要
Java のシリアライズは、オブジェクトをバイト列に変換してファイルやネットワーク経由で受け渡す仕組みです。HttpSession への格納、分散キャッシュへの保存、一時的なスナップショットの書き出しなど、業務システムでも意図せず使っている場面があります。しかし、Serializable を implements するだけで動くように見える手軽さの裏には、serialVersionUID を省略したときの互換性崩壊や、フィールド追加時の予期しない挙動など、テストで落としやすい罠がいくつもあります。この記事では、シリアライズとデシリアライズの基本的な流れを完結したコードで示したうえで、serialVersionUID を明示すべき理由、record をシリアライズするときの注意点、そして実務で避けるべきパターンを整理します。
使いどころ
Servlet コンテナのセッションレプリケーションで HttpSession に格納するオブジェクトを Serializable にする
バッチ処理の途中経過をファイルに書き出しておき、異常終了時に途中から再開できるようにする
分散キャッシュ(Hazelcast、Redis の Java シリアライズモード)にオブジェクトを格納する
コード例
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class SerializationBasicDemo {
// record + Serializable(Java 17+)
record Employee(
String employeeId,
String name,
String department,
int salary
) implements Serializable {
@SuppressWarnings("serial")
private static final long serialVersionUID = 1L;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
var employees = new ArrayList<Employee>();
employees.add(new Employee("E001", "田中太郎", "開発部", 400000));
employees.add(new Employee("E002", "鈴木花子", "営業部", 380000));
var filePath = "employees.dat";
// シリアライズ(オブジェクト → バイト列 → ファイル)
try (var oos = new ObjectOutputStream(new FileOutputStream(filePath))) {
oos.writeObject(employees);
System.out.println("シリアライズ完了: " + filePath);
}
// デシリアライズ(ファイル → バイト列 → オブジェクト)
try (var ois = new ObjectInputStream(new FileInputStream(filePath))) {
@SuppressWarnings("unchecked")
var loaded = (List<Employee>) ois.readObject();
System.out.println("デシリアライズ完了:");
for (var emp : loaded) {
System.out.println(" id=" + emp.employeeId()
+ ", name=" + emp.name()
+ ", dept=" + emp.department()
+ ", salary=" + emp.salary());
}
}
// ファイル削除(クリーンアップ)
new File(filePath).delete();
}
}Version Coverage
record に Serializable を実装できる。record のシリアライズはコンストラクタ経由で復元されるため、writeObject/readObject のカスタマイズが不要になる場面が多い。
// Java 17: record で Serializable を実装
record Employee(
String name, int salary
) implements Serializable {
@SuppressWarnings("serial")
private static final long serialVersionUID = 1L;
}
// コンストラクタ経由で復元されるため安全性が高いLibrary Comparison
注意点
serialVersionUID を省略するとコンパイラがクラス構造からハッシュ値を自動生成する。フィールドやメソッドを追加しただけで値が変わり、過去に保存したデータが InvalidClassException で読めなくなる
Serializable を implements した親クラスのフィールドもシリアライズ対象になる。継承階層が深いと意図しないフィールドまで含まれるため、クラス設計時に影響範囲を把握しておくこと
record は Serializable を自動実装しない。明示的に implements Serializable を書く必要がある。また record のシリアライズはコンストラクタ経由で復元されるため、通常クラスとは挙動が異なる
ObjectOutputStream はストリームヘッダを含むため、同一ファイルに複数回 new ObjectOutputStream で追記するとデシリアライズ時にヘッダの重複で StreamCorruptedException が発生する
FAQ
はい。省略するとクラス構造の変更で値が変わり、過去のデータが読めなくなります。IDE の警告を無効にせず、1L から始めて変更時にインクリメントするのが一般的です。
record はデシリアライズ時にコンストラクタ経由で復元されます。readObject によるフィールド直接書込みが行われないため、コンストラクタでのバリデーションが確実に動作します。
外部システムとのデータ交換や長期保存には向きません。クラス構造の変更に弱く、セキュリティリスクもあるため、JSON や Protocol Buffers への切り替えを検討してください。