CodeGym /コース /JAVA 25 SELF /入れ子および階層オブジェクト: グラフのシリアライズ

入れ子および階層オブジェクト: グラフのシリアライズ

JAVA 25 SELF
レベル 44 , レッスン 1
使用可能

1. コレクションの中のコレクションのシリアライズ

Java のコレクションは、単純な型(たとえば String)だけでなく、ほかのコレクションやオブジェクトも格納できます。これにより複雑な構造を作成できます。たとえば Map<String, List<User>> のように、ここで User は独自クラスです。

例: List を内包する Map のシリアライズ

各ユーザーが友だちリストを持つ、小さなソーシャルネットワークの例を見てみましょう。

import java.io.*;
import java.util.*;

class User implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;

    User(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" + "name='" + name + '\'' + '}';
    }
}

public class SocialNetwork implements Serializable {
    private static final long serialVersionUID = 1L;
    Map<String, List<User>> friends = new HashMap<>();

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SocialNetwork network = new SocialNetwork();
        network.friends.put("alice", Arrays.asList(new User("bob"), new User("carol")));
        network.friends.put("bob", Collections.singletonList(new User("alice")));

        // シリアライズ
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("network.ser"))) {
            out.writeObject(network);
        }

        // デシリアライズ
        SocialNetwork loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("network.ser"))) {
            loaded = (SocialNetwork) in.readObject();
        }

        System.out.println("復元されたネットワーク: " + loaded.friends);
    }
}

言い換えると、使用しているあらゆる型 — HashMapArrayListUser のいずれであれ — はインターフェース Serializable を実装しています。シリアライズ時、Java はすべての入れ子のコレクションやオブジェクトを自動的にたどって同様に書き出します。そのためデシリアライズ後、すべての入れ子のリストを含む完全に復元された構造を得られます。

出力:

復元されたネットワーク: {alice=[User{name='bob'}, User{name='carol'}], bob=[User{name='alice'}]}

入れ子はいくらでも

必要なだけ入れ子にできます: List<List<User>>Map<String, Map<Integer, List<User>>> など — Java は(もちろん常識的な範囲で)再帰を恐れません。

2. 階層オブジェクト: 継承を含むコレクションのシリアライズ

コレクションに継承で構成されたオブジェクトが入っている場合はどうでしょうか。たとえば基本クラス Animal があり、コレクションには CatDog の両方が入っているようなケースです。

例: サブクラスを含むコレクションのシリアライズ

import java.io.*;
import java.util.*;

abstract class Animal implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;

    Animal(String name) {
        this.name = name;
    }

    public abstract String speak();
}

class Cat extends Animal {
    private static final long serialVersionUID = 1L;

    Cat(String name) {
        super(name);
    }

    @Override
    public String speak() {
        return "Meow!";
    }
}

class Dog extends Animal {
    private static final long serialVersionUID = 1L;

    Dog(String name) {
        super(name);
    }

    @Override
    public String speak() {
        return "Woof!";
    }
}

public class Zoo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        List<Animal> animals = new ArrayList<>();
        animals.add(new Cat("ムルカ"));
        animals.add(new Dog("シャーリク"));

        // シリアライズ
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("zoo.ser"))) {
            out.writeObject(animals);
        }

        // デシリアライズ
        List<Animal> loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("zoo.ser"))) {
            loaded = (List<Animal>) in.readObject();
        }

        for (Animal animal : loaded) {
            System.out.println(animal.name + " はこう言う: " + animal.speak());
        }
    }
}

結果:

ムルカ はこう言う: Meow!
シャーリク はこう言う: Woof!

重要なポイント: Java は基底クラスのフィールドだけでなく、オブジェクトの実際の型情報もシリアライズします。したがってデシリアライズ後も「猫」や「犬」としての実体が保持され、メソッドを安全に呼び出せます。

3. オブジェクトグラフのシリアライズ

ここからは本当の魔法 — オブジェクト同士が互いに参照し合い、単なる入れ子ではない関係を持つオブジェクトグラフのシリアライズに進みます。まずはオブジェクトグラフとは何かを押さえましょう。

オブジェクトグラフとは?

オブジェクトグラフとは、フィールド参照を通じてオブジェクト同士が結び付けられている構造のことです。たとえば家系図では、各人物が親、子、兄弟姉妹への参照を持つことがあります。

たとえ話: ソーシャルネットワークの友だち関係を思い浮かべてください。各ユーザーが友だちリストを持ち、その友だちもまたユーザーで、自分の友だちがいます……と続いていきます。これがオブジェクトグラフです。

例: 双方向連結リストのシリアライズ

import java.io.*;

class Node implements Serializable {
    private static final long serialVersionUID = 1L;
    String value;
    Node next;
    Node prev;

    Node(String value) {
        this.value = value;
    }
}

public class DoublyLinkedListDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 2 つのノードを作成して接続する
        Node first = new Node("A");
        Node second = new Node("B");
        first.next = second;
        second.prev = first;

        // シリアライズ
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
            out.writeObject(first);
        }

        // デシリアライズ
        Node loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("list.ser"))) {
            loaded = (Node) in.readObject();
        }

        System.out.println("先頭の値: " + loaded.value); // "A"
        System.out.println("次: " + loaded.next.value);   // "B"
        System.out.println("次の前要素: " + loaded.next.prev.value); // "A"
    }
}

注目すべき点は、シリアライズされるのは 1 本の参照(first)だけですが、再帰的なシリアライズによって Java がすべての関連オブジェクトを「たどる」ことです。デシリアライズ時には参照構造が完全に復元され、loaded.next.prev == loadedtrue になります。ノード同士が互いを参照するような循環があっても、標準の Java シリアライズは正しく動作し、無限ループにはなりません。

4. 入れ子および階層コレクション: 実クラスでの例

モデル: 書籍カタログ

ここでは、通常の書籍と電子書籍(継承)になり得る Book クラスがあるとします。さらに、ジャンルごとのマップ(Map<String, List<Book>>)を持つ Library クラスがあります。各ジャンルは本のリストです。

import java.io.*;
import java.util.*;

abstract class Book implements Serializable {
    private static final long serialVersionUID = 1L;
    String title;

    Book(String title) {
        this.title = title;
    }
}

class PaperBook extends Book {
    private static final long serialVersionUID = 1L;
    int pages;

    PaperBook(String title, int pages) {
        super(title);
        this.pages = pages;
    }
}

class EBook extends Book {
    private static final long serialVersionUID = 1L;
    String format;

    EBook(String title, String format) {
        super(title);
        this.format = format;
    }
}

class Library implements Serializable {
    private static final long serialVersionUID = 1L;
    Map<String, List<Book>> catalog = new HashMap<>();
}

public class CatalogDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Library library = new Library();
        library.catalog.put("SF", Arrays.asList(
                new PaperBook("デューン", 800),
                new EBook("火星の人", "epub")
        ));
        library.catalog.put("古典", Collections.singletonList(
                new PaperBook("戦争と平和", 1200)
        ));

        // シリアライズ
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("library.ser"))) {
            out.writeObject(library);
        }

        // デシリアライズ
        Library loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("library.ser"))) {
            loaded = (Library) in.readObject();
        }

        for (Map.Entry<String, List<Book>> entry : loaded.catalog.entrySet()) {
            System.out.println("ジャンル: " + entry.getKey());
            for (Book book : entry.getValue()) {
                System.out.println(" - " + book.title + " (" + book.getClass().getSimpleName() + ")");
            }
        }
    }
}

出力:

ジャンル: SF
 - デューン (PaperBook)
 - 火星の人 (EBook)
ジャンル: 古典
 - 戦争と平和 (PaperBook)

まとめ:

  • 入れ子のコレクション(Map<String, List<Book>>)のシリアライズは標準で動作します。
  • オブジェクトの型(PaperBookEBook)は保持されます。
  • デシリアライズ後、構造は完全に復元されます。

5. オブジェクトグラフのシリアライズ: 内部では何が起きている?

オブジェクトをシリアライズするとき、Java はそのすべてのフィールド(さらにそのフィールドのフィールド……)をたどり、各オブジェクトを一度だけシリアライズします。同じオブジェクトが再び現れた場合(たとえば循環参照)、Java はそのオブジェクトを再度シリアライズせず、特別な参照を書き込みます。

可視化(フローチャート)

graph TD
    A[オブジェクト A] -- フィールド --> B[オブジェクト B]
    B -- フィールド --> C[オブジェクト C]
    C -- フィールド --> A

Java はまず A をシリアライズし、次に B、続いて C をシリアライズします。そして再び A に出会ったときは「すでにシリアライズ済みのオブジェクト A への参照」を記録します。デシリアライズ時には、すべての関係を保ったまま構造が復元されます。

6. グラフシリアライズの特徴

  • 循環は怖くない: 標準の Java シリアライズは循環参照をサポートし、無限ループにならず、StackOverflowError も引き起こしません。
  • すべてのオブジェクトがシリアライズ可能であること: グラフ内のオブジェクトのうち 1 つでも Serializable を実装していないものがあると、そのオブジェクトでシリアライズは失敗します.
  • 同一オブジェクトは重複しない: グラフ内の複数箇所で同じオブジェクトが参照されている場合でも、デシリアライズ後は同じ参照(同一オブジェクト)になります。
  • オブジェクトの型は保持される: コレクションが List<Animal> として宣言されていても、デシリアライズ後には実クラス(CatDog など)のオブジェクトを得られます。

7. 入れ子および階層オブジェクトをシリアライズするときの典型的な誤り

エラー1: すべてのクラスがシリアライズ可能ではない。
入れ子のオブジェクトやコレクション内にある独自クラスのいずれかに implements Serializable を付け忘れることがとてもよくあります。結果として NotSerializableException が発生し、がっかりすることに。入れ子の連鎖を必ず確認しましょう。

エラー2: 手動シリアライズ時の参照喪失。
writeObject/ readObject を自前で実装する場合、(親への参照や入れ子のコレクションなど)いずれかのフィールドのシリアライズを忘れると、デシリアライズ後に構造が壊れます。復元のテストを必ず行いましょう。

エラー3: 必要なフィールドに transient を付けてしまう。
必要なフィールドを transient でマークすると、そのフィールドはシリアライズストリームに含まれず、復元後は null またはデフォルト値になります。これによりオブジェクトグラフの整合性が損なわれることがあります。

エラー4: シリアライズとデシリアライズの間にクラス構造を変更する。
オブジェクトをシリアライズした後でクラス構造(例: フィールドの追加)を変更すると、デシリアライズ時にエラーが発生したりデータが失われたりする可能性があります。serialVersionUID を使用し、互換性を維持してください。

エラー5: 大きなグラフのシリアライズ。
複雑に相互接続された構造は、非常に大きなファイルサイズや長いシリアライズ/デシリアライズ時間につながることがあります。サイズに注意し、可能であれば分割を検討しましょう。

エラー6: raw 型コレクションのシリアライズ。
generic パラメータなしでコレクションを宣言した場合(たとえば単なる List)、デシリアライズ後に明示的なキャストが必要になり、ClassCastException を招きがちです。ジェネリクスを使用し、型を検証しましょう。

1
タスク
JAVA 25 SELF, レベル 44, レッスン 1
ロック未解除
循環ルートを持つ地下トンネルのマッピング 🚇
循環ルートを持つ地下トンネルのマッピング 🚇
1
タスク
JAVA 25 SELF, レベル 44, レッスン 1
ロック未解除
ハイパーマーケットの品揃え管理:商品の階層とコレクション 🏪
ハイパーマーケットの品揃え管理:商品の階層とコレクション 🏪
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION