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.
GO TO FULL VERSION