CodeGym /Courses /JAVA 25 SELF /Nested and Hierarchical Objects: Serializing Graphs

Nested and Hierarchical Objects: Serializing Graphs

JAVA 25 SELF
Level 44 , Lesson 1
Available

1. Serializing collections inside collections

In Java, collections can contain not only simple types (for example, String) but also other collections or objects. This opens the door to creating complex structures: for example, a Map<String, List<User>>, where User is your own class.

Example: Serializing a Map with a nested List

Let’s consider an example of a small social network where each user has a list of friends.

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")));

        // Serialization
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("network.ser"))) {
            out.writeObject(network);
        }

        // Deserialization
        SocialNetwork loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("network.ser"))) {
            loaded = (SocialNetwork) in.readObject();
        }

        System.out.println("Restored network: " + loaded.friends);
    }
}

You can think of it this way: all the types used — whether HashMap, ArrayList, or User — implement the Serializable interface. During serialization, Java automatically walks through all nested collections and objects, writing them as well. Therefore, after deserialization you get a fully restored structure, including all nested lists.

Output:

Restored network: {alice=[User{name='bob'}, User{name='carol'}], bob=[User{name='alice'}]}

Nesting to your heart’s content

You can create as many levels of nesting as you want: List<List<User>>, Map<String, Map<Integer, List<User>>> — Java isn’t afraid of recursion (within reason, of course).

2. Hierarchical objects: serializing collections with inheritance

What if your collections contain objects built through inheritance? For example, you have a base class Animal, and the collection holds both Cat and Dog.

Example: Serializing a collection with subclasses

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("Murka"));
        animals.add(new Dog("Sharik"));

        // Serialization
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("zoo.ser"))) {
            out.writeObject(animals);
        }

        // Deserialization
        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 + " says: " + animal.speak());
        }
    }
}

Result:

Murka says: Meow!
Sharik says: Woof!

Important note: Java serializes not only the fields of the base class but also information about the object’s actual type. Therefore, after deserialization the objects keep their “feline” or “canine” nature, and you can safely call their methods.

3. Serializing object graphs

Now it’s time to move on to the real magic — serializing object graphs, where objects can reference each other rather than only being nested. But first, let’s figure out what these graphs are.

What is an object graph?

An object graph is a structure where objects can be connected to each other via reference fields. For example, in a family tree each person can have references to parents, children, brothers, and sisters.

Analogy: Imagine a group of friends in a social network: each user has a list of friends, and those friends are also users who have their own friends, and so on. That’s an object graph.

Example: Serializing a doubly linked list

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 {
        // Create two linked nodes
        Node first = new Node("A");
        Node second = new Node("B");
        first.next = second;
        second.prev = first;

        // Serialization
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
            out.writeObject(first);
        }

        // Deserialization
        Node loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("list.ser"))) {
            loaded = (Node) in.readObject();
        }

        System.out.println("First value: " + loaded.value); // "A"
        System.out.println("Next: " + loaded.next.value);   // "B"
        System.out.println("Previous of the next: " + loaded.next.prev.value); // "A"
    }
}

Note that only one reference (first) is serialized, but thanks to recursive serialization Java will “walk” through all connected objects. During deserialization the reference structure is fully restored: loaded.next.prev == loaded will be true! And if the graph contains cycles (for example, when nodes reference each other) Java’s standard serialization works correctly and does not get stuck in an infinite loop.

4. Nested and hierarchical collections: an example with a real class

Model: Book catalog

Suppose we have a Book class that can be a paper book or an electronic edition (inheritance). There’s also a Library class that contains a genre map (Map<String, List<Book>>). Each genre is a list of books.

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("Science Fiction", Arrays.asList(
                new PaperBook("Dune", 800),
                new EBook("The Martian", "epub")
        ));
        library.catalog.put("Classics", Collections.singletonList(
                new PaperBook("War and Peace", 1200)
        ));

        // Serialization
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("library.ser"))) {
            out.writeObject(library);
        }

        // Deserialization
        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("Genre: " + entry.getKey());
            for (Book book : entry.getValue()) {
                System.out.println(" - " + book.title + " (" + book.getClass().getSimpleName() + ")");
            }
        }
    }
}

Output:

Genre: Science Fiction
 - Dune (PaperBook)
 - The Martian (EBook)
Genre: Classics
 - War and Peace (PaperBook)

Summary:

  • Serialization of nested collections (Map<String, List<Book>>) works out of the box.
  • Object types (PaperBook, EBook) are preserved.
  • After deserialization the structure is fully restored.

5. Serializing object graphs: what happens “under the hood”?

When you serialize an object, Java walks through all of its fields (and the fields of those fields, and so on), serializing each object only once. If an object is encountered again (for example, in a cyclic reference), Java writes a special reference instead of serializing it again.

Visualization (flowchart)

graph TD
    A[Object A] -- field --> B[Object B]
    B -- field --> C[Object C]
    C -- field --> A

Java first serializes A, then B, then C, and when it encounters A again, it writes a “reference to the already serialized object A.” During deserialization the structure is restored with all links preserved.

6. Characteristics of object graph serialization

  • Cycles are not a problem: Java’s standard serialization supports cyclic references, does not loop indefinitely, and does not cause StackOverflow.
  • All objects must be serializable: if at least one object in the graph does not implement Serializable, serialization will fail on that object.
  • Identical objects are not duplicated: if the same object appears in multiple places in the graph, after deserialization it will be the very same object (by reference).
  • Object types are preserved: even if a collection is declared as List<Animal>, after deserialization you will get objects of their real classes (Cat, Dog, etc.).

7. Common mistakes when serializing nested and hierarchical objects

Error #1: Not all classes are serializable.
Very often people forget to add implements Serializable to one of their classes that sits inside a collection or a nested object. As a result — NotSerializableException and disappointment. Check the entire nesting chain!

Error #2: Losing references during manual serialization.
If you implement writeObject/readObject yourself and forget to serialize one of the fields (for example, a reference to a parent or to a nested collection), the structure will be corrupted after deserialization. Always test restoration.

Error #3: Using transient for required fields.
If you mark a required field as transient, it will not be written to the serialized stream, and after restoration it will be null or have a default value. This can break the integrity of the object graph.

Error #4: Changing class structure between serialization and deserialization.
If you changed a class’s structure (for example, added a field) after an object was serialized, deserialization may fail or lose data. Use serialVersionUID and maintain compatibility.

Error #5: Serializing large graphs.
Complex interlinked structures can lead to very large files and long serialization/deserialization times. Monitor sizes and split into parts when possible.

Error #6: Serializing raw collections.
If you declared a collection without a generic parameter (for example, just List), you’ll have to cast explicitly after deserialization, which is fraught with ClassCastException. Use generics and validate types.

1
Task
JAVA 25 SELF, level 44, lesson 1
Locked
Mapping underground tunnels with a cyclic route 🚇
Mapping underground tunnels with a cyclic route 🚇
1
Task
JAVA 25 SELF, level 44, lesson 1
Locked
Managing a Hypermarket Catalog: Product Hierarchy and Collections 🏪
Managing a Hypermarket Catalog: Product Hierarchy and Collections 🏪
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION