1. はじめに
自動シリアライズは飛行機のオートパイロットのようなものです。すべてが計画どおりに進む限りはうまく動きます。しかし特別な条件が現れると、単純な仕組みでは足りないことが明らかになります。オブジェクトを保存したいが、すべてのフィールドを保存したくはない状況を想像してください。あるデータは一時的で、あるデータはファイルに書き出すには機微すぎるかもしれません。反対に、保存時に独自の情報(たとえばバージョンやチェックサム)を追加したいこともあります。書き込みや読み込みの前に検証や変換を行う必要があることもあるでしょう。さらに難しいのは、クラスの構造が時間とともに変わっても、以前のバージョンとの互換性を確保したい場合です。
このような場面では、標準のシリアライズだけでは不十分です。自分で制御を握る必要があります。
シリアライズの特別メソッド: writeObject と readObject
Java には、オブジェクトのシリアライズ/デシリアライズ処理を完全に制御できる2つの特別なメソッドがあります。
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
重要!
- メソッドは必ず private(public でも protected でも package-private でもない)でなければなりません。
- シグネチャは上記と完全に一致している必要があります。
- これらのメソッドがクラスに宣言されている場合、標準のシリアライズ/デシリアライズの代わりにそれらが呼び出されます。
どのように動作するのか?
ObjectOutputStream.writeObject(obj) を呼ぶと、JVM はまずクラス obj 内に private void writeObject(ObjectOutputStream) があるかを探します。もしあれば、それが呼び出されます。同様に、デシリアライズ時には private void readObject(ObjectInputStream) が呼ばれます。
これらのメソッドが宣言されていなければ、標準のシリアライズが使われます。
writeObject と readObject の仕組み
メソッドのシグネチャ
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
これらのメソッド内では必ず次を呼び出します:
- out.defaultWriteObject(); — スーパークラスおよび現在のクラスの、標準(transient でない)フィールドをシリアライズするため。
- in.defaultReadObject(); — 標準フィールドをデシリアライズするため。
これらを呼び出さない場合、標準のフィールドはシリアライズされません。デシリアライズ後のオブジェクトは「空っぽ」になります。まるで旅行にパスポートを入れ忘れたスーツケースを持っていくようなものです。形式上は到着できても、身元を証明できません。
2. 例: シリアライズ時にチェックサムを追加する
実用的な例を見てみましょう。ユーザークラスがあり、シリアライズ時にチェックサムをオブジェクトに追加して、デシリアライズ時にデータの完全性を検証したいとします.
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// transient フィールド — シリアライズしない
private transient int checksum;
public User(String name, int age) {
this.name = name;
this.age = age;
this.checksum = calculateChecksum();
}
private int calculateChecksum() {
return (name != null ? name.hashCode() : 0) + age;
}
// カスタムシリアライズ
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 標準フィールドを保存
int sum = calculateChecksum();
out.writeInt(sum); // チェックサムを書き込む
System.out.println("[LOG] User をシリアライズ: checksum=" + sum);
}
// カスタムデシリアライズ
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 標準フィールドを復元
int sum = in.readInt(); // チェックサムを読み込む
int actual = calculateChecksum();
System.out.println("[LOG] User をデシリアライズ: checksum=" + sum + ", actual=" + actual);
if (sum != actual) {
throw new IOException("データが破損しています。チェックサムが一致しません。");
}
this.checksum = actual;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", checksum=" + checksum + "}";
}
}
使用例:
// オブジェクトを保存
User user = new User("Alice", 42);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(user);
}
// オブジェクトを読み込み
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User loaded = (User) in.readObject();
System.out.println("復元したオブジェクト: " + loaded);
}
何が起きているのか?
- シリアライズ時に writeObject が呼ばれ、標準フィールド + チェックサムが保存されます。
- デシリアライズ時に readObject が呼ばれ、フィールドを復元し、チェックサムを検証します。
- コンソールにログが表示され、問題があれば例外がスローされます。
3. 機密データをシリアライズから除外する
特定のフィールド(例: パスワード)をシリアライズしたくない場合があります。そのためにはキーワード transient(詳細は次回の講義)を使うか、writeObject を実装して手動でフィールドを書き込まないようにできます。
例:
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient — シリアライズされない
// あるいは次のようにもできる:
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// password は書き込まない!
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// password は null のまま
}
}
注意:
オブジェクトの一部だけをシリアライズしたいなら、余分なフィールドを書き込まなければよいだけです。
4. スーパークラスの処理: defaultWriteObject と defaultReadObject
独自の writeObject および readObject の中では、ほとんどの場合 defaultWriteObject() と defaultReadObject() を呼び出す必要があります。これは、独自の処理を追加する前に「下書きを保存」するようなものです。
これらのメソッドは、現在のクラスとスーパークラスのすべての非 transient・非 static フィールドの標準的なシリアライズを担当します。これらを呼ばなければ、そのフィールドはシリアライズされず、デシリアライズ時に空になります。
誤った例:
private void writeObject(ObjectOutputStream out) throws IOException {
// out.defaultWriteObject(); // 呼び出しを忘れた!
out.writeInt(123); // 独自の処理のみ
}
この場合、標準フィールドは保存されません!
5. 実践: シリアライズ処理のロギング
ユーザークラスにロギングを追加して、シリアライズとデシリアライズのタイミングを確認してみましょう。
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("[LOG] Person をシリアライズ: " + name + ", 年齢 " + age);
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
System.out.println("[LOG] Person をデシリアライズ: " + name + ", 年齢 " + age);
}
}
使用方法:
Person p = new Person("Bob", 30);
// ファイルに保存
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
out.writeObject(p);
}
// ファイルから読み込み
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"))) {
Person loaded = (Person) in.readObject();
}
結果:
コンソールに、オブジェクトがシリアライズおよびデシリアライズされたことを示すメッセージが表示されます。
6. writeObject/readObject を使う際のよくあるミス
エラー 1: defaultWriteObject/defaultReadObject を呼んでいない。 これらのメソッドを呼び忘れると、標準フィールドはシリアライズされず、デシリアライズ後のオブジェクトは空または不正になります。
エラー 2: メソッドのシグネチャが正しくない。 メソッドは厳密に private void writeObject(ObjectOutputStream) および private void readObject(ObjectInputStream) である必要があります。public/protected にしたり、引数を変更すると自動的には呼ばれません。
エラー 3: メソッド内の例外。 writeObject または readObject 内で例外が発生すると、シリアライズ/デシリアライズは中断され、オブジェクトは正しく保存/読み込みされません。
エラー 4: スーパークラスのシリアライズ/デシリアライズを忘れる。 クラスが別のシリアライズ可能なクラスを継承している場合は、必ず defaultWriteObject/defaultReadObject を呼び出してください。さもないとスーパークラスのフィールドが保存されません。
エラー 5: 機密データをシリアライズしてしまう。 パスワードなどのプライベートなデータを除外し忘れると、シリアライズ済みファイルに含まれてしまいます。transient を使うか、手動で書き込まないようにしましょう。
GO TO FULL VERSION