CodeGym /Cursos /JAVA 25 SELF /Identidad de objetos en la serialización binaria

Identidad de objetos en la serialización binaria

JAVA 25 SELF
Nivel 44, Lección 3
Disponible

1. Introducción

En Java (y en POO en general) cada objeto tiene identidad — no es solo un conjunto de valores de sus campos, sino precisamente «quién es» en memoria. Dos objetos pueden ser «equivalentes» (por ejemplo, a.equals(b) devuelve true), pero ser distintos en identidad (a != b). La identidad es cuando dos variables apuntan al mismo objeto en memoria (a == b).

¿Por qué es importante para la serialización?

Cuando serializas (guardas) un grafo de objetos (por ejemplo, un árbol, una lista o simplemente dos objetos que referencian al mismo objeto anidado), es importante conservar no solo los valores de los campos, sino también la estructura de referencias entre los objetos.

Si en tu grafo hay referencias compartidas (varios objetos referencian al mismo objeto anidado) o incluso referencias cíclicas (A → B → A), entonces, tras la deserialización, esos vínculos deben permanecer iguales.

Ejemplo: tienes dos objetos A y B, ambos referencian al mismo objeto C. Después de serializar y deserializar debe cumplirse: a.c == b.c (el mismo objeto en memoria). Si la serialización simplemente «copia» los objetos, entonces tras la recuperación obtendrás dos objetos C distintos — y ese ya es otro grafo.

2. Cómo ObjectOutputStream resuelve el problema de la identidad

En Java para la serialización binaria se utiliza un par de clases: ObjectOutputStream (para escribir) y ObjectInputStream (para leer).

ObjectOutputStream no es simplemente «escribir un objeto en un archivo». Es inteligente:

  • Hace un seguimiento de todos los objetos que ya se han serializado dentro de un mismo flujo de escritura (una sesión).
  • Si aparece una referencia a un objeto que ya ha sido serializado, no lo escribe de nuevo; en su lugar, escribe una «referencia repetida» (reference handle).
  • Al deserializar, ObjectInputStream reconstruye la estructura para que las referencias repetidas apunten al mismo objeto en memoria.

¡Esto funciona incluso para referencias cíclicas! Es decir, si tienes A → B → A, la serialización y la deserialización no se quedarán en bucle ni «se romperán»: las referencias se restaurarán correctamente.

¿Cómo está implementado?

Internamente, ObjectOutputStream utiliza una tabla especial (identity map), donde se almacenan los objetos ya serializados. Al encontrar un objeto nuevo, se añade a la tabla y se serializa por completo. Al encontrar un objeto que ya está en la tabla, en el flujo se escribe solo una «referencia repetida» (handle).

3. Demostración con un ejemplo

Ejemplo 1: Grafo con referencias cíclicas (A → B, B → A)

import java.io.*;

class Node implements Serializable {
    String name;
    Node next;

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

public class CyclicSerializationDemo {
    public static void main(String[] args) throws Exception {
        Node a = new Node("A");
        Node b = new Node("B");
        a.next = b;
        b.next = a; // ¡Ciclo!

        // Serialización
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cyclic.dat"))) {
            out.writeObject(a);
        }

        // Deserialización
        Node a2;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cyclic.dat"))) {
            a2 = (Node) in.readObject();
        }

        System.out.println("a2.name = " + a2.name); // A
        System.out.println("a2.next.name = " + a2.next.name); // B
        System.out.println("a2.next.next == a2: " + (a2.next.next == a2)); // true!
    }
}

Salida:

a2.name = A
a2.next.name = B
a2.next.next == a2: true

Comentario:
¡El ciclo se ha conservado! Tras la deserialización, a2.next.next vuelve a apuntar a a2.

Ejemplo 2: Referencias compartidas (A → C, B → C)

import java.io.*;

class Wrapper implements Serializable {
    String name;
    Object ref;

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

public class SharedReferenceDemo {
    public static void main(String[] args) throws Exception {
        Wrapper a = new Wrapper("A");
        Wrapper b = new Wrapper("B");
        Wrapper c = new Wrapper("C");

        a.ref = c;
        b.ref = c;

        // Serialización
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("shared.dat"))) {
            out.writeObject(new Wrapper[] {a, b});
        }

        // Deserialización
        Wrapper[] arr;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("shared.dat"))) {
            arr = (Wrapper[]) in.readObject();
        }

        Wrapper a2 = arr[0];
        Wrapper b2 = arr[1];

        System.out.println("a2.ref == b2.ref: " + (a2.ref == b2.ref)); // true!
        System.out.println("a2.ref.name = " + ((Wrapper)a2.ref).name); // C
    }
}

Salida:

a2.ref == b2.ref: true
a2.ref.name = C

Comentario:
Tras la deserialización, ambas referencias vuelven a apuntar al mismo objeto.

4. Relación con writeReplace y readResolve

En Java puedes «intervenir» en el proceso de (de)serialización mediante métodos especiales:

writeReplace() — se invoca antes de la serialización del objeto. Puedes devolver otro objeto, que se serializará en lugar del original.
readResolve() — se invoca después de la deserialización del objeto. Puedes devolver otro objeto, que se usará en lugar del que acaba de crearse.

Impacto en la identidad:

  • Si se utiliza writeReplace() o readResolve(), el objeto devuelto por estos métodos será el que entre en la tabla de referencias.
  • Esto puede cambiar la identidad: si readResolve() devuelve un objeto nuevo/diferente, las referencias compartidas pueden apuntar a instancias distintas.
  • Cuidado: puedes tanto «romper» la identidad como garantizarla deliberadamente (el ejemplo clásico es un Singleton mediante readResolve(), donde todas las referencias tras la deserialización apuntan a la misma instancia singleton).

5. Práctica: código que muestra la preservación de la identidad

import java.io.*;

class Shared implements Serializable {
    String value;
    Shared(String value) { this.value = value; }
}

class Holder implements Serializable {
    String name;
    Shared shared;
    Holder(String name, Shared shared) {
        this.name = name;
        this.shared = shared;
    }
}

public class IdentityDemo {
    public static void main(String[] args) throws Exception {
        Shared c = new Shared("C");
        Holder a = new Holder("A", c);
        Holder b = new Holder("B", c);

        // Serialización
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("identity.dat"))) {
            out.writeObject(new Holder[] {a, b});
        }

        // Deserialización
        Holder[] arr;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("identity.dat"))) {
            arr = (Holder[]) in.readObject();
        }

        Holder a2 = arr[0];
        Holder b2 = arr[1];

        System.out.println("a2.shared == b2.shared: " + (a2.shared == b2.shared)); // true!
        System.out.println("a2.shared.value = " + a2.shared.value); // C
    }
}

Salida:

a2.shared == b2.shared: true
a2.shared.value = C

Comentario:
La identidad del objeto c se ha conservado: tras la deserialización, ambas referencias apuntan a la misma instancia.

6. Conclusiones y errores típicos

Error n.º 1: serializar por separado objetos con referencias compartidas. Si escribes cada objeto con su propia instancia de ObjectOutputStream, las referencias entre ellos no se conservarán — las instancias deserializadas serán diferentes, incluso si en el original era la misma referencia.

Error n.º 2: uso incorrecto de writeReplace() y readResolve(). Estos métodos pueden sustituir el objeto durante la (de)serialización, lo que cambia su identidad. Si no se entiende la mecánica, puedes obtener instancias inesperadas a la salida.

Error n.º 3: efectos inesperados de referencias compartidas mutables. Si varios objetos referencian a un mismo objeto anidado mutable (por ejemplo, una lista), tras la deserialización seguirá siendo así. Un cambio en un lugar afectará a todos los demás.

Error n.º 4: esperar «objetos nuevos» después de la serialización. La serialización no crea una estructura «desde cero» — restaura las referencias originales, tal como estaban. Esto puede resultar inesperado al trabajar con cachés u objetos plantilla.

1
Tarea
JAVA 25 SELF, nivel 44, lección 3
Bloqueada
Trabajo colaborativo con un documento compartido: preservación de objetos compartidos 📝
Trabajo colaborativo con un documento compartido: preservación de objetos compartidos 📝
1
Tarea
JAVA 25 SELF, nivel 44, lección 3
Bloqueada
Gestión de inventario: identidad de objetos al almacenarlos por separado 📦
Gestión de inventario: identidad de objetos al almacenarlos por separado 📦
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION