1. transient をさらに詳しく
Java のキーワード transient は、シリアライザに対して「このフィールドは触らないで。オブジェクトを保存するときは忘れて!」と伝える手段です。フィールドを transient として宣言すると、そのフィールドはシリアライズされたバイトストリームに含まれません。これは、機微情報(例: パスワード)や、保存する必要のない一時的な計算結果に特に有用です。
例: なぜ transient が必要なのか?
ユーザーのクラスがあるとします:
import java.io.Serializable;
public class User implements Serializable {
private String username;
private transient String password; // パスワードは保存したくない!
public User(String username, String password) {
this.username = username;
this.password = password;
}
// ここには getter と setter があります
}
このクラスのオブジェクトをシリアライズすると、password フィールドはファイル(または別のストリーム)に含まれません。つまり、デシリアライズ時にパスワードはデフォルト値になります。オブジェクト型は null、数値は 0、boolean は false です。
実際にはどう動く?
ミニ実験をしてみましょう。まずはユーザーをシリアライズします:
import java.io.*;
public class TransientDemo {
public static void main(String[] args) throws Exception {
User user = new User("vasya", "qwerty123");
// オブジェクトをファイルに保存する
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.ser"));
out.writeObject(user);
out.close();
// 次にオブジェクトを読み戻す
ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.ser"));
User restored = (User) in.readObject();
in.close();
System.out.println("Username: " + restored.username);
System.out.println("Password: " + restored.password);
}
}
結果:
Username: vasya
Password: null
ご覧のとおり、password フィールドは復元されません。これは transient であり、シリアライザがそれを無視したためです。
どこで、なぜ transient を使うのか?
- パスワードとトークン。 決してシリアライズしないでください!
- キャッシュ済みまたは一時的なデータ。 例えば、その場で再計算できるフィールド。
- シリアライズできない/する必要のないオブジェクト。 例: DB 接続、ストリーム、ソケットへの参照。
transient フィールドの挙動の注意点
オブジェクトがデシリアライズされると、transient としてマークされたすべてのフィールドはデフォルト値を取ります。意味を戻す必要がある場合は、readObject メソッドを利用して手動で埋める(キャッシュを再計算する、ユーザーにパスワードを問い合わせる、など)ことができます。
2. serialVersionUID: クラスのバージョンを表す一意の識別子
serialVersionUID は、シリアライズ可能なクラスの「バージョン」を表す特別な long 型の静的フィールドです。シリアライズ時には serialVersionUID の値が書き込まれ、デシリアライズ時に JVM は現在のクラスの値と照合します。一致しない場合は例外がスローされ、オブジェクトは復元されません。
serialVersionUID の宣言方法
とても簡単です:
private static final long serialVersionUID = 1L;
通常は、Serializable を実装するクラス内で宣言します:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
// ... その他のフィールドやメソッド
}
serialVersionUID はなぜ必要?
クラスのオブジェクトをファイルに保存した後で、クラス構造(フィールドの追加、名前変更など)を変更したとします。serialVersionUID が異なると、JVM はそのクラスが古いバージョンと互換でないと判断し、デシリアライズを許可しません。これにより予期せぬ不具合を防げます。
serialVersionUID を宣言しないとどうなる?
serialVersionUID を明示的に宣言しない場合、コンパイラがクラス構造に基づいて自動生成します。しかし、わずかな変更(例えばフィールドの追加・削除)でも serialVersionUID の値は変わります。結果として、古いバージョンのクラスで保存されたオブジェクトをデシリアライズできなくなります。
したがって、serialVersionUID は常に明示的に指定することを推奨します!
デモ: serialVersionUID の不一致
1) まずクラスを作成して、オブジェクトをシリアライズします:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
public User(String username) {
this.username = username;
}
}
2) 次に serialVersionUID を変更します:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 2L; // 以前は 1L、今は 2L!
private String username;
public User(String username) {
this.username = username;
}
}
結果:
java.io.InvalidClassException: User; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
JVM は「バージョンが互換ではありません」と警告します。
serialVersionUID はどんな値を選ぶべき?
多くの場合はシンプルな値(1L、2L、42L など)が使われます。大規模なプロジェクトでは IDE が「長い」値を生成することもあります。重要なのは、クラス構造が非互換に変わったときにだけ変更することです。
3. 実践: transient フィールドと serialVersionUID を実際に使う
例: transient フィールドを持つクラス
学習用アプリ(例えば連絡先マネージャ)を少し改造し、ユーザーのクラスにシリアライズへ含めたくない一時的な認可トークンを保存するフィールドを追加してみましょう。
import java.io.Serializable;
public class Contact implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String phone;
private transient String sessionToken; // 一時トークン
public Contact(String name, String phone, String sessionToken) {
this.name = name;
this.phone = phone;
this.sessionToken = sessionToken;
}
@Override
public String toString() {
return "Contact{" +
"name='" + name + '\'' +
", phone='" + phone + '\'' +
", sessionToken='" + sessionToken + '\'' +
'}';
}
}
実際にシリアライズとデシリアライズを試してみます:
import java.io.*;
public class TransientAndSUIDDemo {
public static void main(String[] args) throws Exception {
Contact c = new Contact("イワン", "+19990001122", "token-12345");
// オブジェクトを保存する
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("contact.ser"));
out.writeObject(c);
out.close();
// オブジェクトを復元する
ObjectInputStream in = new ObjectInputStream(new FileInputStream("contact.ser"));
Contact restored = (Contact) in.readObject();
in.close();
System.out.println("シリアライズ前: " + c);
System.out.println("デシリアライズ後: " + restored);
}
}
出力:
シリアライズ前: Contact{name='イワン', phone='+19990001122', sessionToken='token-12345'}
デシリアライズ後: Contact{name='イワン', phone='+19990001122', sessionToken='null'}
ご覧のとおり、sessionToken フィールドは復元されていません。これは transient だからです。
例: serialVersionUID の実験
1) まず serialVersionUID を 1L にしてオブジェクトをシリアライズします。
2) 次に serialVersionUID を 2L に変更し、同じファイルのデシリアライズを試みます。
結果: 上で示したとおり InvalidClassException が発生します.
4. なぜ serialVersionUID を明示的に指定した方が良いのか
- 明示は暗黙に勝る。 互換性を自分でコントロールできます。クラス構造が本質的に変わっていなければ、従来の serialVersionUID を維持し、オブジェクトは問題なくデシリアライズできます。
- 自動生成は危険になり得る。 どんな変更でも計算された値が変わり、保存済みデータとの互換性が「壊れる」可能性があります。
- IDE が助けてくれる。 多くの IDE(例: IntelliJ IDEA)は serialVersionUID を自動生成できます。
5. transient と serialVersionUID を扱う際の典型的なミス
よくある間違い No.1: 機微なフィールドに transient を付け忘れる。
その結果、パスワードやトークンがうっかりシリアライズ済みファイルに含まれてしまいます。気まずいだけでなく、危険です。
よくある間違い No.2: serialVersionUID を明示宣言しなかった。
クラスを変更したため、古いオブジェクトをデシリアライズできなくなりました。JVM は互換性がないと判断しますが、実際には構造が致命的に変わっていない場合もあります。
よくある間違い No.3: 必要もないのに serialVersionUID を変更した。
getter の追加やコメントの変更だけなら、serialVersionUID を変える必要はありません。そうでないと、古いデータがデシリアライズできなくなります。
よくある間違い No.4: serialVersionUID が static または final でない。
フィールドは private static final long serialVersionUID として宣言する必要があります。そうでないと JVM は正しく認識しません。
よくある間違い No.5: デシリアライズ後に transient フィールドを復元し忘れた。
その値がオブジェクトの動作にとって重要であれば、readObject で復元してください。さもないとオブジェクトが正しく動作しない可能性があります。
GO TO FULL VERSION