CodeGym /Cursos /JAVA 25 SELF /Objetos anidados y jerárquicos: serialización de grafos

Objetos anidados y jerárquicos: serialización de grafos

JAVA 25 SELF
Nivel 44 , Lección 1
Disponible

1. Serialización de colecciones dentro de colecciones

En Java, las colecciones pueden contener no solo tipos simples (por ejemplo, String), sino también otras colecciones u objetos. Esto abre la puerta a crear estructuras complejas: por ejemplo, Map<String, List<User>>, donde User es tu propia clase.

Ejemplo: serialización de Map con List anidado

Veamos un ejemplo de una pequeña red social, donde cada usuario tiene una 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")));

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

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

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

Podemos decirlo así: todos los tipos utilizados —ya sea HashMap, ArrayList o User— implementan la interfaz Serializable. Al serializar, Java recorre automáticamente todas las colecciones y objetos anidados, escribiéndolos también. Por eso, tras la deserialización obtienes la estructura completamente restaurada, incluidos todos los listados anidados.

Salida:

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

Anidamiento a tu gusto

Puedes crear tantos niveles de anidamiento como quieras: List<List<User>>, Map<String, Map<Integer, List<User>>> — Java no teme a la recursión (dentro de lo razonable, claro).

2. Objetos jerárquicos: serialización de colecciones con herencia

¿Qué pasa si tus colecciones contienen objetos construidos con herencia? Por ejemplo, tienes una clase base Animal, y en la colección hay tanto Cat como Dog.

Ejemplo: serialización de una colección con subclases

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

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

        // Deserialización
        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 + " dice: " + animal.speak());
        }
    }
}

Resultado:

Murka dice: Meow!
Sharik dice: Woof!

Punto importante: Java serializa no solo los campos de la clase base, sino también la información sobre el tipo real del objeto. Por lo tanto, tras la deserialización, los objetos conservan su naturaleza de «gato» o «perro», y puedes invocar sus métodos con seguridad.

3. Serialización de grafos de objetos

Ahora es hora de pasar a la verdadera magia — la serialización de grafos de objetos, donde los objetos pueden referenciarse entre sí y no solo estar anidados unos dentro de otros. Pero primero, aclaremos qué es exactamente un grafo.

¿Qué es un grafo de objetos?

Un grafo de objetos es una estructura en la que los objetos pueden estar relacionados entre sí a través de campos de referencia. Por ejemplo, en un árbol genealógico cada persona puede tener referencias a sus padres, hijos y hermanos.

Analogía: Imagina un grupo de amigos en una red social: cada usuario tiene una lista de amistades, y esos amigos también son usuarios que tienen sus propias amistades, y así sucesivamente. Eso es un grafo de objetos.

Ejemplo: serialización de una lista doblemente enlazada

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 {
        // Creamos dos nodos enlazados
        Node first = new Node("A");
        Node second = new Node("B");
        first.next = second;
        second.prev = first;

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

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

        System.out.println("Valor del primero: " + loaded.value); // "A"
        System.out.println("Siguiente: " + loaded.next.value);   // "B"
        System.out.println("Anterior del siguiente: " + loaded.next.prev.value); // "A"
    }
}

Ten en cuenta que solo se serializa una referencia (first), pero gracias a la serialización recursiva Java «recorrerá» todos los objetos enlazados. Al deserializar, la estructura de referencias se restaura por completo: loaded.next.prev == loaded será true. Y si en el grafo hay ciclos (por ejemplo, cuando los nodos se referencian mutuamente), la serialización estándar de Java funciona correctamente y no entra en bucles infinitos.

4. Colecciones anidadas y jerárquicas: ejemplo con una clase real

Modelo: catálogo de libros

Supongamos que tenemos una clase Book, que puede ser un libro en papel o una edición electrónica (herencia). También hay una clase Library, que contiene un mapa de géneros (Map<String, List<Book>>). Cada género es una lista de libros.

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("Ciencia ficción", Arrays.asList(
                new PaperBook("Dune", 800),
                new EBook("El marciano", "epub")
        ));
        library.catalog.put("Clásicos", Collections.singletonList(
                new PaperBook("Guerra y paz", 1200)
        ));

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

        // Deserialización
        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() + ")");
            }
        }
    }
}

Salida:

Género: Ciencia ficción
 - Dune (PaperBook)
 - El marciano (EBook)
Género: Clásicos
 - Guerra y paz (PaperBook)

En resumen:

  • La serialización de colecciones anidadas (Map<String, List<Book>>) funciona «lista para usar».
  • Los tipos de objeto (PaperBook, EBook) se conservan.
  • Tras la deserialización, la estructura se restaura por completo.

5. Serialización de grafos de objetos: ¿qué ocurre «bajo el capó»?

Cuando serializas un objeto, Java «recorre» todos sus campos (y los campos de sus campos, y así sucesivamente), serializando cada objeto solo una vez. Si el objeto aparece de nuevo (por ejemplo, en una referencia cíclica), Java escribe una referencia especial en lugar de serializarlo de nuevo.

Visualización (diagrama de bloques)

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

Java primero serializa A, luego B, después C, y cuando vuelve a encontrar A, escribe «una referencia al objeto A ya serializado». Durante la deserialización, la estructura se restaura conservando todas las relaciones.

6. Particularidades de la serialización de grafos

  • Los ciclos no son un problema: la serialización estándar de Java admite referencias cíclicas, no se queda en bucle y no provoca StackOverflow.
  • Todos los objetos deben ser serializables: si al menos un objeto del grafo no implementa Serializable, la serialización fallará en ese objeto.
  • Los objetos iguales no se duplican: si el mismo objeto aparece en varios lugares del grafo, tras la deserialización será el mismo objeto (por referencia).
  • Los tipos de objeto se conservan: incluso si la colección se declara como List<Animal>, después de deserializar obtendrás objetos de sus clases reales (Cat, Dog, etc.).

7. Errores típicos al serializar objetos anidados y jerárquicos

Error n.º 1: No todos los clases son serializables.
Muy a menudo se olvida añadir implements Serializable a alguna de las clases propias que está dentro de una colección u objeto anidado. Como resultado — NotSerializableException y frustración. ¡Revisa la cadena de anidación!

Error n.º 2: Pérdida de referencias en serialización manual.
Si implementas los métodos writeObject/readObject por tu cuenta y olvidas serializar alguno de los campos (por ejemplo, la referencia al padre o a una colección anidada), después de deserializar la estructura quedará dañada. Prueba siempre la restauración.

Error n.º 3: Uso de transient en campos necesarios.
Si marcas un campo necesario como transient, no se incluirá en el flujo serializado, y tras la restauración será null o tendrá el valor por defecto. Esto puede romper la integridad del grafo de objetos.

Error n.º 4: Cambio en la estructura de clases entre serialización y deserialización.
Si cambiaste la estructura de la clase (por ejemplo, añadiste un campo) después de haber serializado el objeto, al deserializar pueden aparecer errores o pérdida de datos. Usa serialVersionUID y mantén la compatibilidad.

Error n.º 5: Serialización de grafos grandes.
Estructuras complejas e interrelacionadas pueden llevar a archivos muy grandes y a procesos largos de serialización/deserialización. Controla los tamaños y, si es posible, divide en partes.

Error n.º 6: Serialización de colecciones sin genéricos.
Si declaras una colección sin parámetro genérico (por ejemplo, simplemente List), tras la deserialización tendrás que forzar conversiones de tipo, lo que puede derivar en ClassCastException. Usa genéricos y revisa los tipos.

1
Tarea
JAVA 25 SELF, nivel 44, lección 1
Bloqueada
Mapeo de túneles subterráneos con ruta cíclica 🚇
Mapeo de túneles subterráneos con ruta cíclica 🚇
1
Tarea
JAVA 25 SELF, nivel 44, lección 1
Bloqueada
Gestión del surtido del hipermercado: jerarquía y colecciones de productos 🏪
Gestión del surtido del hipermercado: jerarquía y colecciones de productos 🏪
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION