java-recipes

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

Ser-02: transient・serialVersionUID の役割

transient を使うと 特定のフィールドをシリアライズから除外できます。パスワードや秘密鍵など、 ファイルやネットワークに保存したくないセキュリティ情報に活用します。serialVersionUID は クラスのバージョン識別子で、クラス変更時の互換性を管理します。

いつ使うか

  • パスワード・APIキー・セッションIDなど、永続化・送信すべきでないフィールドを除外するとき(transient)
  • クラスにフィールドを追加・削除してもデシリアライズできるよう互換性を保ちたいとき(serialVersionUID)
  • 計算で再現できる値(キャッシュ)はシリアライズを省略してサイズを削減したいとき(transient)

serialVersionUID の役割

状況結果
serialVersionUID を定義しないコンパイラが自動生成。クラス変更で値が変わり、古いファイルを読むと InvalidClassException
serialVersionUID = 1L を定義クラスを変更しても同じ値が維持され、フィールド追加後も古いデータを読み込める
互換性のない変更(フィールドの型変更など)serialVersionUID が同じでも ClassCastException などの実行時エラーになる可能性あり

サンプルコード

TransientSerialVersionSample.java
import java.io.*;

public class TransientSerialVersionSample {

    // serialVersionUID: クラスの「バージョン識別子」
    // 省略するとコンパイラが自動生成するが、クラス変更のたびに変わる危険性がある
    static class UserAccount implements Serializable {
        private static final long serialVersionUID = 1L; // 明示的に定義

        private final String userId;
        private final String email;

        // transient: このフィールドはシリアライズされない
        // パスワード・秘密鍵などのセキュリティ情報は除外すべき
        private transient String password;

        // static フィールドもシリアライズされない(インスタンス固有でないため)
        private static int instanceCount = 0;

        UserAccount(String userId, String email, String password) {
            this.userId = userId;
            this.email = email;
            this.password = password;
            instanceCount++;
        }

        @Override
        public String toString() {
            return "UserAccount{userId='" + userId + "', email='" + email
                    + "', password='" + password + "'}";
        }
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        UserAccount account = new UserAccount("U001", "taro@example.com", "secret123");
        System.out.println("シリアライズ前: " + account);

        // シリアライズ(ByteArray に書き出す)
        byte[] bytes;
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(account);
            bytes = baos.toByteArray();
        }
        System.out.println("シリアライズサイズ: " + bytes.length + " bytes");

        // デシリアライズ
        try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
             ObjectInputStream ois = new ObjectInputStream(bais)) {
            UserAccount loaded = (UserAccount) ois.readObject();
            // password は transient なので null になる
            System.out.println("デシリアライズ後: " + loaded);
            // → password='null' になることを確認
        }
    }
}

transient を付けたフィールドはシリアライズされず、デシリアライズ後は型のデフォルト値(参照型は null、int は 0 など)になります。パスワード・秘密鍵・セッションIDなどのセキュリティ情報は必ず transient にしましょう。static フィールドも同様にシリアライズされません。

よくあるミス・注意点

transient フィールドはデシリアライズ後 null(またはデフォルト値)になる

transient フィールドを持つオブジェクトを シリアライズ後にデシリアライズすると、そのフィールドは参照型ならnull、 int/long などプリミティブ型なら0、 boolean なら false になります。 デシリアライズ後に再設定が必要な場合はreadObject() メソッドをオーバーライドして初期化できます。

serialVersionUID を省略しても動作するが推奨されない

serialVersionUID を省略してもコンパイルは通ります。 しかしクラスにフィールドを1つ追加・削除するだけで自動生成される値が変わり、 古いシリアライズデータが読めなくなります。 長期保存するデータや複数システム間でやり取りするデータには必ず定義しましょう。

record には transient フィールドは書けない

Java 17 以降のrecord の コンポーネント(フィールド)にはtransient を付けられません。 record はすべてのコンポーネントがシリアライズされます。 特定フィールドを除外したい場合は通常の class に Externalizable を実装する方法(Ser-03 参照)か、 JSON など別のフォーマットへの変換を検討しましょう。

テストする観点

  • transient フィールドが、デシリアライズ後に null になること(セキュリティ情報が漏洩しないこと)
  • transient 以外のフィールドが、デシリアライズ後も元の値を保持していること
  • 同じ serialVersionUID でクラスにフィールドを追加しても、旧データが読み込めること(境界値: フィールド追加・削除)
  • 異なる serialVersionUID でシリアライズ・デシリアライズすると InvalidClassException がスローされること
  • static フィールドが、デシリアライズ後にシリアライズ時の値ではなくクラスの現在の値になること

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