CodeGym /Cursos /JAVA 25 SELF /O problema de referências cíclicas: detecção e tratamento...

O problema de referências cíclicas: detecção e tratamento

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

1. O que são referências cíclicas?

Uma referência cíclica é a situação em que um objeto (ou coleção) contém, direta ou indiretamente, uma referência a si mesmo. Em coleções isso ocorre mais do que parece, especialmente se você constrói estruturas de dados complexas ou trabalha com grafos.

Exemplos do dia a dia

  • Dois objetos referenciam um ao outro:
    Por exemplo, você tem a classe User, que possui uma referência a Profile, e Profile tem uma referência de volta para User.
  • A coleção contém a si mesma:
    O exemplo mais simples e “engraçado”:
List<Object> list = new ArrayList<>();
list.add(list); // Opa! a lista contém a si mesma
  • Grafo de objetos:
    Objetos interconectados, por exemplo, nós de uma árvore em que cada um pode ter uma referência ao pai e aos filhos.

Visualização

graph LR
A[User] -- profile --> B[Profile]
B -- user --> A

Ou para uma coleção:

graph TD
L[List] -- add(self) --> L

Por que isso pode ser um problema?

Se o serializador não souber lidar com ciclos, ele pode “entrar no infinito”, tentando serializar objetos aninhados repetidas vezes até estourar a pilha (StackOverflowError). Boa notícia: a serialização padrão do Java conhece esses truques e sabe contorná-los!

2. Como a serialização padrão do Java lida com ciclos?

Quando você serializa um objeto via ObjectOutputStream, o Java rastreia automaticamente quais objetos já foram serializados nesse stream. Se o serializador encontra o objeto novamente, ele não o serializa de novo: grava uma referência especial para o objeto já serializado. Isso permite serializar corretamente até estruturas muito complexas com ciclos.

Exemplo: coleção que contém a si mesma

Vamos tentar serializar uma coleção que contém a si mesma. Não é piada — esse código compila e até funciona:

import java.io.*;
import java.util.*;

public class CyclicListDemo {
    public static void main(String[] args) throws Exception {
        List<Object> list = new ArrayList<>();
        list.add("Hello, cyclic world!");
        list.add(list); // Adicionamos a si mesma

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

        // Desserialização
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cyclic_list.ser"))) {
            List<?> deserialized = (List<?>) in.readObject();

            System.out.println(deserialized.get(0)); // "Hello, cyclic world!"
            System.out.println(deserialized.get(1) == deserialized); // true!
        }
    }
}

Resultado:
— O primeiro elemento é uma string comum.
— O segundo elemento é... a própria coleção! A verificação deserialized.get(1) == deserialized retornará true.
O Java não entrou em loop nem caiu; ele restaurou corretamente a estrutura de referências.

Como isso funciona internamente?

ObjectOutputStream mantém um “registro” interno de objetos serializados. Se um objeto já foi serializado, é gravada no stream uma referência especial (handle) para ele, e não o seu conteúdo. Na desserialização, ObjectInputStream restaura exatamente as mesmas ligações.

3. Problemas e limitações

  • Serializar acidentalmente um grafo enorme.
    Se sua estrutura de dados for muito grande e contiver muitas referências cruzadas, a serialização pode levar muito tempo e gerar um arquivo gigantesco.
  • Mudança na estrutura das classes.
    Se você serializou um objeto e depois alterou sua classe (por exemplo, adicionou ou removeu um campo), na desserialização pode ocorrer InvalidClassException. Especialmente se mudarem campos que participam do ciclo.
  • Problemas na serialização customizada.
    Se você implementar manualmente os métodos writeObject e readObject, precisa tratar os ciclos corretamente por conta própria. Se esquecer de chamar os métodos padrão (defaultWriteObject/defaultReadObject), o serializador não conseguirá rastrear os ciclos.
  • Serialização para outros formatos (por exemplo, JSON).
    A serialização padrão do Java (ObjectOutputStream) lida com ciclos, mas se você serializa objetos em JSON (por exemplo, com Jackson ou Gson), os ciclos podem levar a StackOverflowError ou exceções. Essas bibliotecas, por padrão, não lidam com ciclos — são necessárias configurações explícitas.

4. Como contornar referências cíclicas

Na serialização padrão do Java

Tudo funciona “de fábrica”! Você não precisa fazer nada especial — o Java detectará os ciclos e preservará a estrutura das referências.

Manual: serialização para outros formatos

  • Usar identificadores em vez de referências.
    Em vez de armazenar referências para outros objetos, armazene seus identificadores únicos. Após a desserialização, reconstrua as ligações por esses IDs.
  • Anotações ou configurações específicas.
    No Jackson, você pode usar as anotações @JsonIdentityInfo ou a dupla @JsonBackReference/@JsonManagedReference para controlar a serialização de ciclos.
  • Remover ciclos antes da serialização.
    Zere temporariamente campos que criam o ciclo, exclua-os com transient ou por meio de anotações.

Exemplo: serializando um grafo com ciclos

Vamos considerar um exemplo com uma estrutura mais complexa — um grafo de usuários, em que cada usuário pode ser amigo de outro usuário.

import java.io.*;
import java.util.*;

class User implements Serializable {
    String name;
    List<User> friends = new ArrayList<>();

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

    public String toString() {
        return name + " (" + friends.size() + " friends)";
    }
}

public class CyclicGraphDemo {
    public static void main(String[] args) throws Exception {
        User alice = new User("Alice");
        User bob = new User("Bob");
        User charlie = new User("Charlie");

        // Criamos amizades com ciclos
        alice.friends.add(bob);
        bob.friends.add(charlie);
        charlie.friends.add(alice); // ciclo!

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

        // Desserialização
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("users.ser"))) {
            User restoredAlice = (User) in.readObject();
            System.out.println(restoredAlice);
            System.out.println(restoredAlice.friends.get(0));
            System.out.println(restoredAlice.friends.get(0).friends.get(0));
            System.out.println(restoredAlice.friends.get(0).friends.get(0).friends.get(0) == restoredAlice); // true!
        }
    }
}

Resultado:
— A estrutura com ciclo é restaurada: após três passos pelos amigos chegamos novamente em Alice.
— O Java não se confundiu nem entrou em loop.

5. Erros comuns ao lidar com referências cíclicas

Erro nº 1: serialização em JSON sem suporte a ciclos. Se você decidir serializar um objeto com ciclos via Jackson ou Gson sem configuração, muito provavelmente obterá StackOverflowError. Por exemplo, se você tem a classe Node, em que cada nó referencia o pai e os filhos, a serialização dessa árvore em JSON levará a aninhamento infinito.

Erro nº 2: quebra da estrutura das classes. Se após a serialização você alterar a estrutura da classe (por exemplo, adicionar um campo), na desserialização do arquivo antigo pode ocorrer um erro de incompatibilidade. Isso é especialmente crítico para grafos complexos com ciclos.

Erro nº 3: serialização artesanal sem considerar ciclos. Se você implementa writeObject/readObject manualmente e não chama defaultWriteObject, o Java não conseguirá rastrear os ciclos, e a serialização pode entrar em loop ou a estrutura de referências pode se quebrar na desserialização.

Erro nº 4: inserção acidental da coleção nela mesma. Às vezes, desenvolvedores iniciantes adicionam acidentalmente uma coleção a ela mesma (por exemplo, ao copiar elementos), sem perceber que criaram um ciclo. Como resultado, a serialização até funcionará, mas a lógica do programa pode se tornar estranha e imprevisível.

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