概要

Java のシリアライズは、オブジェクトをバイト列に変換してファイルやネットワーク経由で受け渡す仕組みです。HttpSession への格納、分散キャッシュへの保存、一時的なスナップショットの書き出しなど、業務システムでも意図せず使っている場面があります。しかし、Serializable を implements するだけで動くように見える手軽さの裏には、serialVersionUID を省略したときの互換性崩壊や、フィールド追加時の予期しない挙動など、テストで落としやすい罠がいくつもあります。この記事では、シリアライズとデシリアライズの基本的な流れを完結したコードで示したうえで、serialVersionUID を明示すべき理由、record をシリアライズするときの注意点、そして実務で避けるべきパターンを整理します。

使いどころ

Servlet コンテナのセッションレプリケーションで HttpSession に格納するオブジェクトを Serializable にする

バッチ処理の途中経過をファイルに書き出しておき、異常終了時に途中から再開できるようにする

分散キャッシュ(Hazelcast、Redis の Java シリアライズモード)にオブジェクトを格納する

コード例

SerializationBasicDemo.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();
    }
}

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

Version Coverage

record に Serializable を実装できる。record のシリアライズはコンストラクタ経由で復元されるため、writeObject/readObject のカスタマイズが不要になる場面が多い。

Java 17
// Java 17: record で Serializable を実装
record Employee(
    String name, int salary
) implements Serializable {
    @SuppressWarnings("serial")
    private static final long serialVersionUID = 1L;
}
// コンストラクタ経由で復元されるため安全性が高い

Library Comparison

標準 API(ObjectOutputStream / ObjectInputStream)Java 間だけでオブジェクトを受け渡す場面。HttpSession やインメモリキャッシュのシリアライズなど。Java 以外の言語とのデータ交換には使えない。クラス構造の変更に弱い。
Jackson(JSON)REST API やファイル出力など、人間が読める形式でデータを交換したいとき。型情報が失われるため、ポリモーフィズムの復元には @JsonTypeInfo 等の設定が必要。バイナリより容量が大きい。
Protocol Buffers異言語間のデータ交換やスキーマの厳密な管理が求められるとき。.proto ファイルの管理とコード生成のビルド設定が必要。小規模なプロジェクトでは導入コストが見合わない。

注意点

serialVersionUID を省略するとコンパイラがクラス構造からハッシュ値を自動生成する。フィールドやメソッドを追加しただけで値が変わり、過去に保存したデータが InvalidClassException で読めなくなる

Serializable を implements した親クラスのフィールドもシリアライズ対象になる。継承階層が深いと意図しないフィールドまで含まれるため、クラス設計時に影響範囲を把握しておくこと

record は Serializable を自動実装しない。明示的に implements Serializable を書く必要がある。また record のシリアライズはコンストラクタ経由で復元されるため、通常クラスとは挙動が異なる

ObjectOutputStream はストリームヘッダを含むため、同一ファイルに複数回 new ObjectOutputStream で追記するとデシリアライズ時にヘッダの重複で StreamCorruptedException が発生する

FAQ

serialVersionUID は必ず明示すべきですか。

はい。省略するとクラス構造の変更で値が変わり、過去のデータが読めなくなります。IDE の警告を無効にせず、1L から始めて変更時にインクリメントするのが一般的です。

record のシリアライズは通常クラスと何が違いますか。

record はデシリアライズ時にコンストラクタ経由で復元されます。readObject によるフィールド直接書込みが行われないため、コンストラクタでのバリデーションが確実に動作します。

シリアライズを避けたほうがよい場面はありますか。

外部システムとのデータ交換や長期保存には向きません。クラス構造の変更に弱く、セキュリティリスクもあるため、JSON や Protocol Buffers への切り替えを検討してください。

関連書籍

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

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