CodeGym /Cursos /JAVA 25 SELF /Objetos aninhados e hierárquicos: serialização de grafos

Objetos aninhados e hierárquicos: serialização de grafos

JAVA 25 SELF
Nível 44, Lição 1
Disponível

1. Serialização de coleções dentro de coleções

Em Java, coleções podem conter não apenas tipos simples (por exemplo, String), mas também outras coleções ou objetos. Isso abre espaço para criar estruturas complexas: por exemplo, Map<String, List<User>>, em que User é sua própria classe.

Exemplo: serialização de Map com List aninhada

Vamos considerar o exemplo de uma pequena rede social, em que cada usuário tem uma lista de amigos.

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

        // Serialização
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("network.ser"))) {
            out.writeObject(network);
        }

        // Desserialização
        SocialNetwork loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("network.ser"))) {
            loaded = (SocialNetwork) in.readObject();
        }

        System.out.println("Rede restaurada: " + loaded.friends);
    }
}

Podemos dizer assim: todos os tipos usados — seja HashMap, ArrayList ou User — implementam a interface Serializable. Ao serializar, o Java percorre automaticamente todas as coleções e objetos aninhados, gravando-os também. Portanto, após a desserialização, você obtém a estrutura totalmente restaurada, incluindo todas as listas internas.

Saída:

Rede restaurada: {alice=[User{name='bob'}, User{name='carol'}], bob=[User{name='alice'}]}

Aninhamento para todos os gostos

Você pode criar quantos níveis de aninhamento quiser: List<List<User>>, Map<String, Map<Integer, List<User>>> — o Java não teme recursão (dentro do razoável, claro).

2. Objetos hierárquicos: serialização de coleções com herança

E se suas coleções contiverem objetos baseados em herança? Por exemplo, você tem a classe base Animal, e na coleção há tanto Cat quanto Dog?

Exemplo: serialização de coleção com herdeiros

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

        // Serialização
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("zoo.ser"))) {
            out.writeObject(animals);
        }

        // Desserialização
        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 + " diz: " + animal.speak());
        }
    }
}

Resultado:

Murka diz: Meow!
Sharik diz: Woof!

Ponto importante: O Java serializa não apenas os campos da classe base, mas também a informação sobre o tipo real do objeto. Portanto, após a desserialização, os objetos mantêm sua natureza “felina” ou “canina”, e você pode chamar seus métodos com segurança.

3. Serialização de grafos de objetos

Agora é hora de passar para a verdadeira mágica — a serialização de grafos de objetos, em que os objetos podem referenciar uns aos outros, e não apenas estar aninhados entre si. Mas antes, vamos entender o que são esses grafos.

O que é um grafo de objetos?

Um grafo de objetos é uma estrutura em que os objetos podem estar conectados entre si por meio de campos de referência. Por exemplo, em uma árvore genealógica, cada pessoa pode ter referências aos pais, filhos, irmãos e irmãs.

Analogia: Imagine um grupo de amigos em uma rede social: cada usuário tem uma lista de amigos, e esses amigos também são usuários que têm seus próprios amigos, e assim por diante. Isso é um grafo de objetos.

Exemplo: serialização de uma lista duplamente encadeada

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 {
        // Criamos dois nós conectados
        Node first = new Node("A");
        Node second = new Node("B");
        first.next = second;
        second.prev = first;

        // Serialização
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
            out.writeObject(first);
        }

        // Desserialização
        Node loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("list.ser"))) {
            loaded = (Node) in.readObject();
        }

        System.out.println("Valor do primeiro: " + loaded.value); // "A"
        System.out.println("Próximo: " + loaded.next.value);   // "B"
        System.out.println("Anterior do próximo: " + loaded.next.prev.value); // "A"
    }
}

Observe que apenas uma referência (first) é serializada, mas, graças à serialização recursiva, o Java “percorre” todos os objetos conectados. Na desserialização, a estrutura de referências é totalmente restaurada: loaded.next.prev == loaded será true! E, se houver ciclos no grafo (por exemplo, quando nós se referenciam mutuamente), a serialização padrão do Java funciona corretamente e não entra em loop.

4. Coleções aninhadas e hierárquicas: exemplo com uma classe real

Modelo: catálogo de livros

Suponha que temos a classe Book, que pode ser um livro impresso ou uma edição eletrônica (herança). Também há a classe Library, que contém um mapa de gêneros (Map<String, List<Book>>). Cada gênero é uma lista de livros.

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("Ficção científica", Arrays.asList(
                new PaperBook("Duna", 800),
                new EBook("O Marciano", "epub")
        ));
        library.catalog.put("Clássicos", Collections.singletonList(
                new PaperBook("Guerra e Paz", 1200)
        ));

        // Serialização
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("library.ser"))) {
            out.writeObject(library);
        }

        // Desserialização
        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("Gênero: " + entry.getKey());
            for (Book book : entry.getValue()) {
                System.out.println(" - " + book.title + " (" + book.getClass().getSimpleName() + ")");
            }
        }
    }
}

Saída:

Gênero: Ficção científica
 - Duna (PaperBook)
 - O Marciano (EBook)
Gênero: Clássicos
 - Guerra e Paz (PaperBook)

Em resumo:

  • A serialização de coleções aninhadas (Map<String, List<Book>>) funciona “pronto para uso”.
  • Os tipos dos objetos (PaperBook, EBook) são preservados.
  • Após a desserialização, a estrutura é totalmente restaurada.

5. Serialização de grafos de objetos: o que acontece “por baixo do capô”?

Quando você serializa um objeto, o Java “caminha” por todos os seus campos (e pelos campos desses campos, e assim por diante), serializando cada objeto apenas uma vez. Se um objeto aparecer novamente (por exemplo, em uma referência cíclica), o Java grava uma referência especial, e não o serializa de novo.

Visualização (fluxograma)

graph TD
    A[Objeto A] -- campo --> B[Objeto B]
    B -- campo --> C[Objeto C]
    C -- campo --> A

O Java primeiro serializa A, depois B, depois C e, quando encontra A novamente, grava uma “referência ao objeto A já serializado”. Na desserialização, a estrutura é restaurada mantendo todas as ligações.

6. Características da serialização de grafos

  • Ciclos não são um problema: a serialização padrão do Java suporta referências cíclicas, não entra em loop e não gera StackOverflow.
  • Todos os objetos devem ser serializáveis: se ao menos um objeto no grafo não implementar Serializable, a serialização falhará nesse objeto.
  • Objetos idênticos não são duplicados: se o mesmo objeto aparecer em vários lugares do grafo, após a desserialização ele será o mesmo objeto (por referência).
  • Os tipos dos objetos são preservados: mesmo que a coleção seja declarada como List<Animal>, após a desserialização você obterá objetos de suas classes reais (Cat, Dog, etc.).

7. Erros comuns na serialização de objetos aninhados e hierárquicos

Erro nº 1: Nem todas as classes são serializáveis.
Muitas vezes esquecemos de adicionar implements Serializable a uma das classes próprias que está dentro da coleção ou do objeto aninhado. Como resultado — NotSerializableException e frustração. Verifique a cadeia de aninhamento!

Erro nº 2: Perda de referências na serialização manual.
Se você implementar os métodos writeObject/readObject manualmente e esquecer de serializar um dos campos (por exemplo, a referência ao pai ou a uma coleção aninhada), após a desserialização a estrutura ficará corrompida. Sempre teste a restauração.

Erro nº 3: Uso de transient em campos necessários.
Se você marcar um campo necessário como transient, ele não será incluído no fluxo serializado e, após a restauração, ficará null ou terá o valor padrão. Isso pode quebrar a integridade do grafo de objetos.

Erro nº 4: Mudança da estrutura das classes entre serialização e desserialização.
Se você alterou a estrutura da classe (por exemplo, adicionou um campo) depois de serializar o objeto, na tentativa de desserialização podem ocorrer erros ou perda de dados. Use serialVersionUID e mantenha a compatibilidade.

Erro nº 5: Serialização de grafos grandes.
Estruturas altamente interligadas podem gerar arquivos muito grandes e tornar a serialização/desserialização lenta. Monitore os tamanhos e, se possível, divida em partes.

Erro nº 6: Serialização de coleções “raw”.
Se você declarou uma coleção sem parâmetro genérico (por exemplo, apenas List), após a desserialização será necessário fazer cast explícito de tipos, o que pode levar a ClassCastException. Use generics e verifique os tipos.

1
Tarefa
JAVA 25 SELF, nível 44, lição 1
Bloqueado
Mapeamento de túneis subterrâneos com rota cíclica 🚇
Mapeamento de túneis subterrâneos com rota cíclica 🚇
1
Tarefa
JAVA 25 SELF, nível 44, lição 1
Bloqueado
Gestão do sortimento do hipermercado: hierarquia e coleções de produtos 🏪
Gestão do sortimento do hipermercado: hierarquia e coleções de produtos 🏪
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION