1. はじめに
Java でオブジェクトをシリアライズするには、一般的にインターフェース Serializable を使います。これは簡単で、インターフェースを実装するだけで ObjectOutputStream/ObjectInputStream を使ってオブジェクトを保存/読み込みできます。しかし、これだけでは不十分な場合があります。
- どのフィールドをどのようにシリアライズするかを完全に制御したい。
- クラスの異なるバージョン間の互換性を確保したい。
- シリアライズ済みファイルのサイズを小さくしたい、または処理を高速化したい。
このような場合に備えて、Java にはインターフェース Externalizable があります。より「手動」で柔軟なシリアライゼーション手法です。
要点:
- Serializable — 自動シリアライゼーション: 何をどのように書き込むかは Java が決めます。
- Externalizable — 手動シリアライゼーション: 何をどのように保存/復元するかを自分で指定します.
2. 契約 Externalizable: writeExternal と readExternal を実装する
Externalizable を使うには、次のことが必要です。
- インターフェース java.io.Externalizable を実装する。
- 必ず次の 2 つのメソッドを実装する:
- void writeExternal(ObjectOutput out) throws IOException
- void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
例:
import java.io.*;
public class User implements Externalizable {
private String name;
private int age;
// 引数なしの public コンストラクタが必須!
public User() {}
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
重要: どのフィールドをどの順序でシリアライズするかは開発者が決めます。ただし必須要件が 1 つあります。クラスには public の引数なしコンストラクタが必要です。これがないと、デシリアライズ時に InvalidClassException が発生します。
3. Externalizable をいつ使うべきか?
Externalizable を使うのは次のような場合:
- データ形式を完全にコントロールしたい。 例えば、一部のフィールドだけをシリアライズしたい、特定の順序/形式でシリアライズしたい、など。
- 性能やファイルサイズの最適化。 標準のシリアライゼーションは付随情報(メタデータ、クラス名、型など)を追加します。Externalizable なら必要なデータだけを書き出せます。
- 後方互換性の確保。 クラス構造が変わっても、古い/新しいデータ形式を読み取るロジックを手動で実装できます。
- 標準的でないオブジェクトのシリアライズ。 例えば、標準の方法ではシリアライズできないフィールド(transient、volatile、複雑な構造など)がある場合。
いつ使うべきではないか?
- 完全な制御が不要な場合 — Serializable を使いましょう。その方が簡単で安全です。
- クラス変更時にデータ形式の互換性を維持できる自信がない場合。
4. Externalizable の長所と短所 Serializable と比較
長所:
- シリアライゼーションを完全に制御できる。 何をどのように書き出し/読み込むかを自分で決められる。
- コンパクト。 余計なメタデータがなく、データ本体のみ。
- 高速。 データ量が少ないほど、入出力が速い。
- 柔軟性。 異なるフォーマットのバージョンに対応したり、圧縮・暗号化などを実装できる。
短所:
- 手作業の実装ゆえにミスしやすい。 書き込み/読み取りの順序を取り違えると、シリアライズが「壊れ」(エラーや不正なデータになる)。
- 自動サポートがない transient、serialVersionUID。すべてを自分で設計・実装する必要がある。
- 保守が難しい。 クラス構造を変更したら、シリアライズ用メソッドの更新を忘れてはならない。
- 引数なしの public コンストラクタが必須。
- 「魔法」が少ない分、責任が重くなる。
5. 例: シンプルなオブジェクトのシリアライズ/デシリアライズ
オブジェクトのシリアライズ
User user = new User("Alice", 30);
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); // Alice (30)
}
注意: フィールドの書き込み/読み取り順序を変えたり、あるフィールドのシリアライズを忘れたりすると、データは不正になります。writeExternal と readExternal の処理順序は厳密に一致させなければなりません。
例: 一部のフィールドだけをシリアライズする
public class SecretUser implements Externalizable {
private String login;
private transient String password; // Externalizable では transient は意味を持たない
public SecretUser() {}
public SecretUser(String login, String password) {
this.login = login;
this.password = password;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(login);
// パスワードはシリアライズしない!
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
login = in.readUTF();
password = null; // パスワードは復元しない
}
}
6. 実践: シリアライズされたファイルサイズの比較
Serializable と Externalizable でシリアライズした場合に、ファイルのサイズがどれくらい違うかを比較してみましょう。
Serializable を使うクラス
public class UserSerializable implements Serializable {
private String name;
private int age;
public UserSerializable(String name, int age) {
this.name = name;
this.age = age;
}
}
Externalizable を使うクラス
public class UserExternalizable implements Externalizable {
private String name;
private int age;
public UserExternalizable() {}
public UserExternalizable(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
}
}
比較用コード
import java.io.*;
public class CompareSerialization {
public static void main(String[] args) throws Exception {
UserSerializable s = new UserSerializable("Bob", 25);
UserExternalizable e = new UserExternalizable("Bob", 25);
// Serializable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
out.writeObject(s);
}
// Externalizable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ext.bin"))) {
out.writeObject(e);
}
System.out.println("Serializable file size: " + new File("ser.bin").length());
System.out.println("Externalizable file size: " + new File("ext.bin").length());
}
}
結果:
ファイル ser.bin(Serializable)は通常サイズが大きく、Java の付随情報を含みます。ファイル ext.bin(Externalizable)はデータ本体のみで、通常はより小さくなります。
7. Externalizable を扱う際の典型的なミス
ミス1: 引数なしの public コンストラクタがない。
Externalizable を実装するクラスには必ず public の引数なしコンストラクタが必要です。これがないとデシリアライズ時に InvalidClassException が発生します。
ミス2: フィールドの書き込み/読み取り順序を崩す。
メソッド writeExternal と readExternal は同じ順序で動作しなければなりません。書き込みで先に name を保存し、読み取りで先に age を読み込もうとすると、データが破損します。
ミス3: シリアライズ時にフィールドを書き忘れる。
writeExternal でフィールドの書き込みを忘れると、デシリアライズ時にその値は参照型なら null、数値型なら 0 になります。
ミス4: transient や serialVersionUID の誤用。
Serializable と異なり、Externalizable ではこれらの仕組みは自動では機能しません。どのフィールドを保存するかしないかを自分で管理する必要があります。
ミス5: メソッドを更新せずにクラス構造を変更する。
フィールドを追加・削除しても、writeExternal と readExternal に対応する変更を加えないと、過去に保存したデータが正しく読み込めなくなる可能性があります。
GO TO FULL VERSION