1. Introdução: serialização de objetos com campos de referência
Em aplicações reais, raramente há classes completamente “planas”. Normalmente, um objeto contém outros objetos e, por sua vez, esses podem conter mais alguma coisa. Isso é chamado de composição (ou aninhamento) de objetos. Por exemplo:
public class Address {
String city;
String street;
}
public class Person {
String name;
int age;
Address address; // Objeto aninhado!
}
Quando serializamos um objeto assim, surge a pergunta: o que fazer com o campo address? O Java deve serializá-lo junto com Person? E se dentro de Address houver mais algum objeto? Felizmente (ou infelizmente — depende da situação), o Java, por padrão, serializa todos os objetos aninhados de forma recursiva, se eles também implementarem a interface Serializable.
A serialização em Java é sempre uma serialização profunda (deep serialization). Isso significa que não apenas o próprio objeto é serializado, mas também todos os objetos aos quais ele se refere por meio de seus campos (que não sejam transient) e assim por diante — até “o fundo”.
Visualização do processo
graph TD
A[Person] --> B[Address]
B --> C[CityInfo]
A --> D[Pet]
Em suma, se você decidiu serializar Person, o Java serializará também Address e tudo que estiver dentro de Address, e assim por diante.
2. Requisitos para objetos aninhados: Serializable é obrigatório!
Você provavelmente já percebeu um ponto importante: todos os objetos aninhados que serão serializados também devem implementar a interface Serializable.
Se ao menos um campo de referência aponta para um objeto que não implementa Serializable, a tentativa de serialização terminará com a exceção java.io.NotSerializableException.
Vamos considerar alguns exemplos de serialização profunda.
Exemplo: tudo certo
import java.io.Serializable;
public class Address implements Serializable {
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Ambas as classes implementam Serializable. Tudo funciona; a serialização ocorre com sucesso.
Exemplo: erro!
public class Address { // NÃO implementa Serializable!
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Como vemos, aqui Address não implementa a interface Serializable. Portanto, ao serializar Person, obteremos a exceção NotSerializableException.
3. Exemplo: serialização e desserialização com objeto aninhado
Vamos ver como isso funciona na prática. No nível anterior, trabalhamos com o aplicativo “Gerenciador de contatos”. Agora vamos adicionar o endereço de cada usuário.
import java.io.*;
class Address implements Serializable {
String city;
String street;
Address(String city, String street) {
this.city = city;
this.street = street;
}
}
class Person implements Serializable {
String name;
int age;
Address address;
Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
}
public class SerializationDemo {
public static void main(String[] args) throws Exception {
Person p = new Person("Ivan", 30, new Address("Praga", "Slavinska, 1"));
// Serialização
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Desserialização
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name + ", " + restored.age + ", " +
restored.address.city + ", " + restored.address.street);
}
}
Resultado:
Ivan, 30, Praga, Slavinska, 1
Tudo funciona: o objeto aninhado Address foi serializado e restaurado junto com Person.
4. E se o objeto aninhado não for serializável?
Se você tentar serializar um objeto cujo pelo menos um campo de referência aponta para um objeto que não implementa Serializable, o Java lançará uma exceção já na tentativa de serialização.
Demonstração do erro
class Address { // não é Serializable!
String city;
String street;
}
class Person implements Serializable {
String name;
Address address;
}
public class Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.name = "Petya";
p.address = new Address();
p.address.city = "Derry";
p.address.street = "Vyazov";
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p); // <-- HAVERÁ EXCEÇÃO!
out.close();
}
}
Erro:
java.io.NotSerializableException: Address
5. Transient para objetos aninhados
O que fazer se você tiver um campo de referência para um objeto que não deve ser serializado (por exemplo, um cache, uma conexão com banco de dados, um objeto temporário)? Nesse caso, declare o campo como transient. Assim, o Java simplesmente ignorará esse campo durante a serialização.
Exemplo com transient
class Address { // não é Serializable
String city;
String street;
}
class Person implements Serializable {
String name;
transient Address address; // transient!
Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
public class Test {
public static void main(String[] args) throws Exception {
Address addr = new Address();
addr.city = "Los Santos";
addr.street = "Mulholland Drive";
Person p = new Person("Sasha", addr);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Desserialização
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // "Sasha"
System.out.println(restored.address); // null!
}
}
Resultado:
O campo address após a desserialização é null, porque ele é transient.
6. Aninhamento profundo e recursão
A serialização funciona de forma recursiva: se Person tem um campo Address, e Address tem um campo CityInfo, e assim por diante, o serializador vai “mergulhando” cada vez mais fundo até encontrar algo não serializável ou até acabar a memória (brincadeira, mas só pela metade).
Importante: referências cíclicas
O serializador do Java sabe lidar com referências cíclicas. Se, por exemplo, um objeto tem uma referência para outro objeto que, por sua vez, referencia o primeiro, o serializador não entra em loop; ele preserva a estrutura corretamente.
class A implements Serializable {
B b;
}
class B implements Serializable {
A a;
}
Se você criar objetos A e B que apontam um para o outro, a serialização não causará StackOverflowError — o Java lembra objetos já serializados.
7. Exemplo: serialização de um objeto com lista aninhada
Muitas vezes, dentro de um objeto há coleções de outros objetos. Por exemplo, um usuário pode ter uma lista de amigos:
import java.io.*;
import java.util.*;
class Person implements Serializable {
String name;
List<Person> friends;
Person(String name) {
this.name = name;
this.friends = new ArrayList<>();
}
}
public class FriendsSerialization {
public static void main(String[] args) throws Exception {
Person alice = new Person("Alice");
Person bob = new Person("Bob");
alice.friends.add(bob);
// Serialização
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("friends.ser"));
out.writeObject(alice);
out.close();
// Desserialização
ObjectInputStream in = new ObjectInputStream(new FileInputStream("friends.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // Alice
System.out.println(restored.friends.get(0).name); // Bob
}
}
Importante: As coleções da biblioteca padrão do Java (ArrayList, HashMap etc.) implementam Serializable, então tudo funciona “pronto para uso”.
8. Erros comuns ao serializar objetos aninhados
Erro nº 1: Um dos objetos aninhados não implementa Serializable. Você esqueceu de adicionar implements Serializable em uma das classes aninhadas. Resultado — NotSerializableException na primeira tentativa de serialização. Verifique se todas as classes na sua cadeia de serialização suportam essa interface.
Erro nº 2: Campo não serializável não foi declarado como transient. Se você tem um campo que não deve ser serializado (por exemplo, um stream, uma conexão com banco de dados, algo temporário), mas não o declarou como transient, a serialização falhará com erro. Não se esqueça do transient!
Erro nº 3: Incompatibilidade de serialVersionUID em classes aninhadas. Se você declarar explicitamente serialVersionUID em classes aninhadas e alterar sua estrutura, lembre-se de atualizar esse identificador — caso contrário, podem ocorrer erros durante a desserialização.
Erro nº 4: Objetos aninhados mutáveis (alteráveis). Se você serializa uma coleção ou um objeto que depois muda (por exemplo, a lista de amigos), após a desserialização aquilo será apenas um “instantâneo” do momento da serialização. Novas mudanças no original não se refletirão no objeto desserializado.
Erro nº 5: Serialização de grafos de objetos enormes. Se você tiver uma estrutura muito complexa com muitos objetos aninhados, a serialização pode consumir bastante tempo e memória. Às vezes é melhor serializar apenas os dados essenciais, e não toda a estrutura.
GO TO FULL VERSION