1. はじめに: 参照フィールドを持つオブジェクトのシリアライズ
実際のアプリケーションで完全に「フラット」なクラスはまれです。通常、オブジェクトは別のオブジェクトを含み、それらもさらに何かを含むことがあります。これはオブジェクトのコンポジション(ネスト)と呼ばれます。例えば:
public class Address {
String city;
String street;
}
public class Person {
String name;
int age;
Address address; // ネストされたオブジェクト!
}
このようなオブジェクトをシリアライズする場合、address フィールドはどう扱われるべきでしょうか? Java は Person と一緒にそれもシリアライズすべきでしょうか? さらに Address の内部に別のオブジェクトがあったら? 幸い(あるいは状況によっては不幸に)、Java はデフォルトで、Serializable インターフェースを実装している限り、すべてのネストされたオブジェクトを再帰的にシリアライズします。
Java におけるシリアライズは常にディープシリアライズです。つまり、オブジェクト本体だけでなく、その(transient ではない)フィールドが参照するすべてのオブジェクトも、その先の「底」までシリアライズされます。
プロセスの可視化
graph TD
A[Person] --> B[Address]
B --> C[CityInfo]
A --> D[Pet]
要するに、Person をシリアライズするなら、Address も、その内部のすべても、その先も連鎖的にシリアライズされます。
2. ネストされたオブジェクトの要件: Serializable は必須!
すでにお気づきのとおり、シリアライズされるネストされたオブジェクトはすべて、インターフェース Serializable を実装していなければなりません。
少なくとも 1 つの参照フィールドが Serializable を実装していないオブジェクトを指していると、シリアライズの試行は java.io.NotSerializableException 例外で終了します。
ディープシリアライズの例を見てみましょう。
例: 問題なし
import java.io.Serializable;
public class Address implements Serializable {
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
両方のクラスが Serializable を実装しています。すべて正常に動作し、シリアライズは成功します。
例: エラー!
public class Address { // Serializable を実装していない!
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
ここでは Address が Serializable を実装していません。したがって Person をシリアライズしようとすると、NotSerializableException が発生します。
3. 例: ネストされたオブジェクトを含むシリアライズ/デシリアライズ
実際にどのようになるか見てみましょう。前回のレベルでは「連絡先マネージャー」アプリで作業しました。今回は各ユーザーに住所を追加してみましょう。
import java.io.*;
class Address implements Serializable {
String city;
String street;
Address(String city, String street) {
this.city = city;
this.street = street;
}
}
class Person implements Serializable {
String name;
int age;
Address address;
Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
}
public class SerializationDemo {
public static void main(String[] args) throws Exception {
Person p = new Person("イワン", 30, new Address("プラハ", "スラヴィンスカ, 1"));
// シリアライズ
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// デシリアライズ
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name + ", " + restored.age + ", " +
restored.address.city + ", " + restored.address.street);
}
}
結果:
イワン, 30, プラハ, スラヴィンスカ, 1
問題なく動作します:ネストされたオブジェクト Address も Person と共にシリアライズ・復元されました。
4. ネストされたオブジェクトがシリアライズ不能な場合は?
少なくとも 1 つの参照フィールドが Serializable を実装していないオブジェクトを指している状態でシリアライズを試みると、Java は試行時点で例外を投げます。
エラーのデモ
class Address { // Serializable ではない!
String city;
String street;
}
class Person implements Serializable {
String name;
Address address;
}
public class Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.name = "ペーチャ";
p.address = new Address();
p.address.city = "デリー";
p.address.street = "ヴィャゾフ";
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p); // <-- ここで例外が発生!
out.close();
}
}
エラー:
java.io.NotSerializableException: Address
5. ネストされたオブジェクトに対する transient
シリアライズすべきではない(例えば、キャッシュ、データベース接続、一時オブジェクト)オブジェクトへの参照フィールドがある場合はどうすればよいでしょうか? その場合はフィールドを transient として宣言します。すると Java はシリアライズ時にそのフィールドを単にスキップします。
transient の例
class Address { // Serializable ではない
String city;
String street;
}
class Person implements Serializable {
String name;
transient Address address; // transient!
Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
public class Test {
public static void main(String[] args) throws Exception {
Address addr = new Address();
addr.city = "ロスサントス";
addr.street = "マルホランド・ドライブ";
Person p = new Person("サーシャ", addr);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// デシリアライズ
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // "サーシャ"
System.out.println(restored.address); // null!
}
}
結果:
デシリアライズ後、フィールド address は null になります。これはそれが transient だからです。
6. 深いネストと再帰
シリアライズは再帰的に動作します。Person に Address フィールドがあり、Address に CityInfo フィールドがあり…という場合、シリアライザは、シリアライズ不能なものに出会うか、メモリが尽きるまで(冗談ですが半分本気)どんどん深く「潜って」いきます。
重要: 循環参照
Java のシリアライザは循環参照を扱えます。例えば、あるオブジェクトが別のオブジェクトを参照し、そのオブジェクトが元に戻って参照する場合でも、シリアライザはループせず、構造を正しく保存します。
class A implements Serializable {
B b;
}
class B implements Serializable {
A a;
}
相互に参照する A と B を作成しても、シリアライズで StackOverflowError にはなりません。Java はすでにシリアライズしたオブジェクトを記憶しています。
7. 例: ネストされたリストを持つオブジェクトのシリアライズ
オブジェクト内部に他のオブジェクトのコレクションがあることはよくあります。例えば、ユーザーには友達のリストがあるかもしれません。
import java.io.*;
import java.util.*;
class Person implements Serializable {
String name;
List<Person> friends;
Person(String name) {
this.name = name;
this.friends = new ArrayList<>();
}
}
public class FriendsSerialization {
public static void main(String[] args) throws Exception {
Person alice = new Person("Alice");
Person bob = new Person("Bob");
alice.friends.add(bob);
// シリアライズ
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("friends.ser"));
out.writeObject(alice);
out.close();
// デシリアライズ
ObjectInputStream in = new ObjectInputStream(new FileInputStream("friends.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // Alice
System.out.println(restored.friends.get(0).name); // Bob
}
}
重要: Java 標準ライブラリのコレクション(ArrayList、HashMap など)は Serializable を実装しているため、すぐに動作します。
8. ネストされたオブジェクトのシリアライズでの典型的な誤り
誤り1: ネストされたオブジェクトの一つが Serializable を実装していない。 ネストされたクラスのいずれかに implements Serializable を付け忘れています。結果として、最初のシリアライズ試行で NotSerializableException が発生します。シリアライズチェーン上のすべてのクラスがこのインターフェースをサポートしているか確認しましょう。
誤り2: シリアライズ不可のフィールドを transient にしていない。 シリアライズすべきでないフィールド(例えばストリーム、データベース接続、一時的なもの)があるのに、それを transient として宣言していないと、シリアライズはエラーで失敗します。transient を忘れないでください。
誤り3: ネストされたクラスでの serialVersionUID の不一致。 ネストされたクラスで serialVersionUID を明示的に宣言していて、その構造を変更する場合、この識別子の更新を忘れないでください。さもないとデシリアライズ時にエラーになる可能性があります。
誤り4: 変更されうる(ミュータブルな)ネストされたオブジェクト。 コレクションや、後から変更されるオブジェクト(例:友達リスト)をシリアライズした場合、デシリアライズ後はシリアライズ時点の「スナップショット」になります。元のオブジェクトに対する新しい変更は、デシリアライズしたオブジェクトには反映されません。
誤り5: 巨大なオブジェクトグラフのシリアライズ。 非常に複雑で多数のネストを含む構造では、シリアライズに多くの時間とメモリがかかることがあります。場合によっては、構造全体ではなく、主要なデータだけをシリアライズした方がよいこともあります。
GO TO FULL VERSION