CodeGym /コース /JAVA 25 SELF /オブジェクトのシリアライズ入門:なぜ必要か

オブジェクトのシリアライズ入門:なぜ必要か

JAVA 25 SELF
レベル 42 , レッスン 0
使用可能

1. シリアライズが必要な理由

自分のオブジェクトを、休暇に持っていく荷物だと想像してください。シリアライズとは、そのスーツケースの中身をすべて特別なコンテナに詰め、預け入れたり郵送したりできるようにすることです。デシリアライズはその逆で、そのコンテナを開封して中身を元の姿で取り出すことです。

本質的には、シリアライズはオブジェクトをバイトストリームに変換し、ファイルに保存したり、ネットワークで送ったり、メモリに保持したりできるようにします。デシリアライズはその逆で、このストリームからオブジェクトを復元します。ごく単純化すると、シリアライズはオブジェクトを「冷凍」して、あとで「解凍」して同じ状態に戻すようなものです。

プログラムの起動間でオブジェクトの状態を保持する

よくあるシナリオのひとつが、プログラムの状態の保存です。たとえば、ユーザーの一覧、ゲームの結果、アプリの設定など。これらはオブジェクトのまま保持しておくのが便利です。データが起動のたびに失われないよう、ファイルにシリアライズし、次回起動時にデシリアライズします。

わかりやすい例が、一般的なゲームのセーブです。プレイヤーがステージを進めると、その進捗が「冷凍」され、シリアライズによってファイルに書き込まれます。翌日ゲームを起動すると、進捗が「解凍」され、ファイルのデータが再びオブジェクトになって、前回の続きからプレイできます。

このような簡単なセーブを作ってみましょう:

import java.io.*;

// プレイヤークラスは Serializable である必要があります
class Player implements Serializable {
    String name;
    int score;

    Player(String name, int score) {
        this.name = name;
        this.score = score;
    }
}

public class GameSaveExample {
    public static void main(String[] args) throws Exception {
        // プレイヤーのオブジェクトを作成
        Player player = new Player("Ihor", 1500);

        // --- 保存(シリアライズ) ---
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("save.dat"))) {
            out.writeObject(player);
            System.out.println("進捗が保存されました!");
        }

        // --- 読み込み(デシリアライズ) ---
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("save.dat"))) {
            Player loaded = (Player) in.readObject();
            System.out.println("進捗を読み込みました: " + loaded.name + "、スコア " + loaded.score);
        }
    }
}

注意: このコードを動かすには、クラス Player がインターフェース Serializable を実装している必要があります。詳しくは次の講義で!

  • Player は名前とスコアのフィールドを持つ通常のクラスで、インターフェース Serializable を実装しています(implements Serializable)。
  • ObjectOutputStream はオブジェクトをファイル "save.dat" に書き込みます。
  • ObjectInputStream は同じオブジェクトを読み戻します。
  • その結果、本物のセーブになります。次回の起動時、プログラムは同じ状態のプレイヤーオブジェクトを読み込みます。

ネットワーク越しや JVM 間でのオブジェクトの送受信

分散システムでは、異なるプログラム間や別マシン間でオブジェクトをやり取りする必要がよくあります。たとえば、クライアントとサーバーがメッセージを交換する場合です。シリアライズにより、一方でオブジェクトを「梱包」してネットワーク経由で送り、もう一方で「開梱」できます。

例: クライアントが注文オブジェクト(Order)をサーバーへ送信し、サーバーはそれを受信してデシリアライズし、処理します。

Java テクノロジーでの利用

  • RMI (Remote Method Invocation): リモートオブジェクトのメソッド呼び出しを可能にします。引数や戻り値を送るためにシリアライズが必要です。
  • HTTP セッション: サーブレットでは、コンテナ再起動時にセッション内のオブジェクトがシリアライズされます。
  • JMS (Java Message Service): コンポーネント間のメッセージはシリアライズされる場合があります。
  • キャッシュ: オブジェクトをキャッシュ(ディスクや分散ストア)に保存するためにシリアライズすることがあります。

キャッシュと可搬性

中間結果を素早く保存したい場合(たとえばキャッシュ用途)、シリアライズはとても有効です。オブジェクトをシリアライズしてディスクやメモリに保存し、再計算せずにすばやく復元できます。

2. シリアライズの利用シナリオ例

ユーザーコレクションをファイルに保存する

たとえば、次のような User クラスがあるとします:

public class User {
    String name;
    int age;
    // ... 他のフィールド
}

そしてユーザーのリストがあります:

List<User> users = new ArrayList<>();
users.add(new User("ヴァーシャ", 25));
users.add(new User("マーシャ", 30));
// ... などなど

このリストをファイルに保存するには、シリアライズします。必要になったらデシリアライズすれば、同じユーザーが入った同じリストが得られます。なお、クラス User(およびそのフィールドすべて)はシリアライズをサポートする、すなわち Serializable を実装している必要があります。

クライアントとサーバー間でメッセージを送る

典型例はチャットです。ユーザーがメッセージを書き、Message オブジェクトがシリアライズされてネットワークへ送られます。サーバーはバイトストリームを受け取り、オブジェクトをデシリアライズして処理し、必要に応じて転送します。

import java.io.*;
import java.net.*;

// Message は Serializable である必要があります
class Message implements Serializable {
    String text;

    Message(String text) {
        this.text = text;
    }
}

// サーバー
class Server {
    public static void main(String[] args) throws Exception {
        try (ServerSocket serverSocket = new ServerSocket(5000)) {
            System.out.println("サーバーが接続を待機中...");
            Socket socket = serverSocket.accept();
            System.out.println("クライアントが接続しました!");

            try (ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
                Message msg = (Message) in.readObject();
                System.out.println("メッセージを受信: " + msg.text);
            }
        }
    }
}

// クライアント
class Client {
    public static void main(String[] args) throws Exception {
        try (Socket socket = new Socket("localhost", 5000)) {
            try (ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream())) {
                Message msg = new Message("こんにちは、サーバー!");
                out.writeObject(msg);
                System.out.println("メッセージを送信しました!");
            }
        }
    }
}

仕組み:

  1. まず Server を起動します(接続を待機します)。
  2. 次に Client を起動します("localhost:5000" に接続します)。
  3. クライアントは Message オブジェクトをシリアライズしてソケット経由で送信します。
  4. サーバーはバイトストリームを受け取り、デシリアライズしてテキストを出力します。

ここではソケット(ServerSocketSocket)を使っています。これはネットワーク通信の仕組みで、詳しくは後で学びます。ここで大事なのはネットワークの細部ではなく発想そのものです。クライアントはオブジェクト Message を作り、シリアライズして送信する。サーバーはバイトストリームを受け取り、オブジェクトにデシリアライズしてメッセージを表示する。つまり、ServerSocketSocket が何者かまだ分からなくても、この例が示すのはシリアライズの価値です。オブジェクトを「梱包」してネットワークで送信し、相手側で余計な変換なしに開梱できるのです。

オブジェクトのキャッシュ

大規模アプリでは高速化のためにキャッシュをよく使います。たとえば、重い計算の結果をシリアライズしてキャッシュ(ファイル、データベース、分散ストア)に保存します。次回のリクエストでは、オブジェクトをデシリアライズするだけで結果をすばやく復元できます。

import java.io.*;

// キャッシュしたい計算結果
class Result implements Serializable {
    int value;

    Result(int value) {
        this.value = value;
    }
}

public class CacheExample {
    private static final String CACHE_FILE = "cache.dat";

    public static void main(String[] args) throws Exception {
        Result result;

        // キャッシュが存在するか確認
        File file = new File(CACHE_FILE);
        if (file.exists()) {
            // キャッシュから結果を読み込み
            try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) {
                result = (Result) in.readObject();
                System.out.println("キャッシュから読み込み: " + result.value);
            }
        } else {
            // 「重い」計算(例としては単に数の二乗)
            int x = 12345;
            System.out.println("計算中...(時間がかかります)");
            result = new Result(x * x);

            // 結果をキャッシュに保存
            try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) {
                out.writeObject(result);
                System.out.println("キャッシュに保存しました: " + result.value);
            }
        }
    }
}

3. シリアライズの制約とリスク

シリアライズは強力ですが、落とし穴もあります。主な制約とリスクを見ていきましょう。

すべてのオブジェクトがシリアライズできるわけではない

Java では、すべてのオブジェクトが「そのまま」シリアライズできるわけではありません。たとえば、外部リソース(ファイル、ネットワーク接続、入出力ストリーム)に結び付いたオブジェクトはシリアライズできません。これは当然で、「開いたファイル」や「生きている」ネットワーク接続は OS や実行環境に依存する状態を持つため、シリアライズ不可能です。

例: FileInputStream 型のフィールドを持つクラスはシリアライズできません。シリアライズしようとするとエラーになります。

セキュリティ上の問題

シリアライズは潜在的なセキュリティホールになり得ます。信頼できないソース(例えばインターネット)から受け取ったデータをデシリアライズすると、攻撃者が細工したバイトストリームにより、プログラムが予期せぬ動作をしたり、場合によっては悪意あるコードが実行されたりすることがあります。

ルール: 信頼できないソースのデータをデシリアライズしてはいけません! 見知らぬ差出人からの荷物を受け取るのと同じで、中身が何であるか分かりません。

バージョン互換性

クラスの構造(例えばフィールドの追加・削除)を変更すると、以前にシリアライズしたオブジェクトが新しいバージョンのクラスと互換でなくなる可能性があります。これによりデシリアライズ時にエラーが発生することがあります。詳細は次回以降の講義で扱います。

パフォーマンス

Java のバイナリシリアライズは十分高速ですが、常に最もコンパクトとは限らず、他言語との連携に必ずしも便利とは言えません。外部システムとのやり取りには、テキスト形式(JSON、XML)がよく使われます。

4. 初学者が陥りがちなシリアライズのミス

ミス1: インターフェース Serializable を実装していないオブジェクトをシリアライズしようとする。
結果として NotSerializableException が発生します。クラスで明示的に implements Serializable と記述し、すべてのフィールドもシリアライズ可能であることを確認しましょう!

ミス2: シリアライズできないフィールドを持つオブジェクトをシリアライズする。
クラスにシリアライズ非対応の型(例: ストリームや DB 接続)のフィールドがあると、シリアライズは失敗します。対処法は、そのようなフィールドに transient を付けることです(詳細は後述)。

ミス3: 信頼できないソースからのデータをデシリアライズする。
これはセキュリティ上の脆弱性や、悪意あるコードの実行につながる可能性があります。自分のプログラムがシリアライズしたデータだけを信頼しましょう!

ミス4: シリアライズ後にクラス構造を変更する。
オブジェクトを保存した後でクラスにフィールドの追加・削除を行うと、デシリアライズ時にエラーや「奇妙な」値の発生を招くことがあります。詳細は次回以降の講義で扱います。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION