CodeGym /Cursos /JAVA 25 SELF /Identidade de objetos na serialização binária

Identidade de objetos na serialização binária

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

1. Introdução

Em Java (e, de modo geral, na POO) cada objeto tem uma identidade — não é apenas o conjunto de valores de seus campos, mas sim “quem ele é” na memória. Dois objetos podem ser “equivalentes” (por exemplo, a.equals(b) retorna true), mas diferentes em identidade (a != b). Identidade é quando duas variáveis apontam para o mesmo objeto na memória (a == b).

Por que isso é importante para a serialização?

Quando você serializa (salva) um grafo de objetos (por exemplo, uma árvore, uma lista ou simplesmente dois objetos que referenciam o mesmo objeto aninhado), é importante preservar não só os valores dos campos, mas também a estrutura de referências entre os objetos.

Se no seu grafo houver referências repetidas (vários objetos referenciam o mesmo objeto aninhado) ou até referências cíclicas (A → B → A), então, após a desserialização, esses vínculos devem permanecer os mesmos.

Exemplo: você tem dois objetos A e B, ambos referenciam o mesmo objeto C. Após a serialização e a desserialização, deve valer: a.c == b.c (o mesmo objeto na memória). Se a serialização simplesmente “copiar” os objetos, após a restauração sairão dois objetos C diferentes — e isso já é outro grafo.

2. Como o ObjectOutputStream resolve o problema da identidade

No Java, para serialização binária, usa-se um par de classes: ObjectOutputStream (para escrita) e ObjectInputStream (para leitura).

ObjectOutputStream não é apenas “gravar um objeto em arquivo”. Ele é inteligente:

  • Acompanha todos os objetos que já foram serializados dentro de um mesmo fluxo de escrita (uma sessão).
  • Se aparece uma referência para um objeto que já foi serializado, ele não o grava de novo; grava uma “referência repetida” (reference handle).
  • Na desserialização, o ObjectInputStream restaura a estrutura de modo que as referências repetidas apontem para o mesmo objeto na memória.

Isso funciona até para referências cíclicas! Ou seja, se você tiver A → B → A, a serialização e a desserialização não entrarão em loop nem “quebrarão”: as referências serão restauradas corretamente.

Como isso é implementado?

Por dentro, o ObjectOutputStream usa uma tabela especial (identity map) onde ficam os objetos já serializados. Ao encontrar um objeto novo, ele é adicionado à tabela e serializado por completo. Ao encontrar um objeto já presente na tabela, grava-se no fluxo apenas a “referência repetida” (handle).

3. Demonstração com exemplos

Exemplo 1: Grafo com referências 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!

        // Serialização
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cyclic.dat"))) {
            out.writeObject(a);
        }

        // Desserialização
        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!
    }
}

Saída:

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

Comentário:
O ciclo foi preservado! Após a desserialização, a2.next.next volta a apontar para a2.

Exemplo 2: Referências repetidas (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;

        // Serialização
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("shared.dat"))) {
            out.writeObject(new Wrapper[] {a, b});
        }

        // Desserialização
        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
    }
}

Saída:

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

Comentário:
Após a desserialização, ambas as referências voltam a apontar para o mesmo objeto.

4. Relação com writeReplace e readResolve

No Java é possível “intervir” no processo de (de)serialização por meio de métodos especiais:

writeReplace() — é chamado antes da serialização do objeto. É possível retornar outro objeto, que será serializado no lugar do original.
readResolve() — é chamado após a desserialização do objeto. É possível retornar outro objeto, que será usado em vez daquele recém-criado.

Impacto na identidade:

  • Se for usado writeReplace() ou readResolve(), o objeto retornado por esses métodos é que entrará na tabela de referências.
  • Isso pode alterar a identidade: se readResolve() retornar um objeto novo/diferente, as referências repetidas podem passar a apontar para instâncias distintas.
  • Tenha cuidado: você pode tanto “quebrar” a identidade quanto garanti-la de propósito (exemplo clássico — Singleton via readResolve(), em que todas as referências após a desserialização apontam para o mesmo singleton).

5. Prática: código que demonstra a preservação da identidade

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

        // Serialização
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("identity.dat"))) {
            out.writeObject(new Holder[] {a, b});
        }

        // Desserialização
        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
    }
}

Saída:

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

Comentário:
A identidade do objeto c foi preservada: após a desserialização, ambas as referências apontam para a mesma instância.

6. Conclusões e erros comuns

Erro №1: serializar objetos com referências repetidas separadamente. Se você grava cada objeto com sua própria instância de ObjectOutputStream, as referências entre eles não serão preservadas — as instâncias desserializadas serão diferentes, mesmo que no original fosse a mesma referência.

Erro №2: uso incorreto de writeReplace() e readResolve(). Esses métodos podem substituir o objeto durante a (de)serialização, o que altera sua identidade. Sem entender a mecânica, você pode acabar com instâncias inesperadas no resultado.

Erro №3: efeitos inesperados de referências mutáveis compartilhadas. Se vários objetos referenciam um mesmo objeto aninhado mutável (por exemplo, uma lista), após a desserialização continuará assim. Alterações em um lugar afetarão todos os demais.

Erro №4: esperar “novos” objetos após a serialização. A serialização não cria uma estrutura “nova” — ela restaura as referências originais, como no original. Isso pode ser surpreendente ao trabalhar com caches ou objetos de modelo.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION