CodeGym /コース /JAVA 25 SELF /Externalizable: シリアライゼーションのきめ細かな調整

Externalizable: シリアライゼーションのきめ細かな調整

JAVA 25 SELF
レベル 43, レッスン 2
使用可能

1. はじめに

Java でオブジェクトをシリアライズするには、一般的にインターフェース Serializable を使います。これは簡単で、インターフェースを実装するだけで ObjectOutputStream/ObjectInputStream を使ってオブジェクトを保存/読み込みできます。しかし、これだけでは不十分な場合があります。

  • どのフィールドをどのようにシリアライズするかを完全に制御したい。
  • クラスの異なるバージョン間の互換性を確保したい。
  • シリアライズ済みファイルのサイズを小さくしたい、または処理を高速化したい。

このような場合に備えて、Java にはインターフェース Externalizable があります。より「手動」で柔軟なシリアライゼーション手法です。

要点:

  • Serializable — 自動シリアライゼーション: 何をどのように書き込むかは Java が決めます。
  • Externalizable — 手動シリアライゼーション: 何をどのように保存/復元するかを自分で指定します.

2. 契約 Externalizable: writeExternal と readExternal を実装する

Externalizable を使うには、次のことが必要です。

  1. インターフェース java.io.Externalizable を実装する。
  2. 必ず次の 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 に対応する変更を加えないと、過去に保存したデータが正しく読み込めなくなる可能性があります。

1
タスク
JAVA 25 SELF, レベル 43, レッスン 2
ロック未解除
ユーザープロファイル:データ書き込みの制御
ユーザープロファイル:データ書き込みの制御
1
タスク
JAVA 25 SELF, レベル 43, レッスン 2
ロック未解除
ナビゲーション座標:順序のゲーム
ナビゲーション座標:順序のゲーム
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION