1. Introdução
Serialização automática é como o piloto automático de um avião: funciona muito bem enquanto tudo sai conforme o plano. Mas basta surgirem condições especiais para ficar claro que o mecanismo simples já não é suficiente. Imagine que você precisa salvar um objeto, mas não todos os seus campos: alguns dados são temporários e outros são sensíveis demais para serem gravados em arquivo. Ou, ao contrário — ao salvar, é preciso adicionar algo seu: por exemplo, uma versão ou um checksum. Às vezes, antes de gravar ou carregar os dados, é necessário realizar uma verificação ou transformação. E, às vezes, a tarefa é ainda mais complexa: garantir compatibilidade com versões anteriores da classe, caso sua estrutura mude com o tempo.
É nessas situações que fica claro: a serialização padrão sozinha não dá conta. É preciso assumir o controle.
Métodos especiais de serialização: writeObject e readObject
No Java, há dois métodos especiais que permitem controlar completamente o processo de serialização e desserialização de um objeto:
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
Importante!
- Os métodos devem ser exatamente private (não public, não protected, não package-private).
- As assinaturas devem coincidir com as apresentadas acima.
- Se esses métodos forem declarados na sua classe, eles serão chamados em vez da serialização/desserialização padrão.
Como isso funciona?
Quando você chama ObjectOutputStream.writeObject(obj), a JVM primeiro procura na classe de obj o método private void writeObject(ObjectOutputStream). Se ele existir — é ele que será chamado. Da mesma forma, durante a desserialização é chamado private void readObject(ObjectInputStream).
Se os métodos não estiverem declarados, usa-se a serialização padrão.
Como writeObject e readObject são estruturados
Assinaturas dos métodos
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
Dentro desses métodos, você deve obrigatoriamente chamar:
- out.defaultWriteObject(); — para serializar os campos padrão (não transient) da superclasse e da classe atual.
- in.defaultReadObject(); — para desserializar os campos padrão.
Se você não chamar esses métodos, os campos padrão não serão serializados — e, na desserialização, o objeto ficará “vazio”. É como esquecer de colocar o passaporte na mala: formalmente você chegou, mas não consegue provar quem é.
2. Exemplo: adicionando um checksum durante a serialização
Vamos ver um exemplo prático. Suponha que temos uma classe de usuário e queremos, na serialização, adicionar ao objeto um checksum para verificar a integridade dos dados na desserialização.
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// campo transient — não o serializamos
private transient int checksum;
public User(String name, int age) {
this.name = name;
this.age = age;
this.checksum = calculateChecksum();
}
private int calculateChecksum() {
return (name != null ? name.hashCode() : 0) + age;
}
// Serialização personalizada
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // Salva os campos padrão
int sum = calculateChecksum();
out.writeInt(sum); // Grava o checksum
System.out.println("[LOG] Serialização de User: checksum=" + sum);
}
// Desserialização personalizada
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Restaura os campos padrão
int sum = in.readInt(); // Lê o checksum
int actual = calculateChecksum();
System.out.println("[LOG] Desserialização de User: checksum=" + sum + ", atual=" + actual);
if (sum != actual) {
throw new IOException("Dados corrompidos! O checksum não confere.");
}
this.checksum = actual;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", checksum=" + checksum + "}";
}
}
Exemplo de uso:
// Salva o objeto
User user = new User("Alice", 42);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(user);
}
// Carrega o objeto
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User loaded = (User) in.readObject();
System.out.println("Objeto restaurado: " + loaded);
}
O que acontece?
- Na serialização, writeObject é chamado; os campos padrão são salvos + o checksum.
- Na desserialização, readObject é chamado; os campos são restaurados + o checksum é verificado.
- Um log aparecerá no console e, se algo estiver errado, uma exceção será lançada.
3. Excluindo dados sensíveis da serialização
Às vezes é necessário que certos campos não sejam serializados (por exemplo, senhas). Para isso, você pode usar a palavra‑chave transient (mais detalhes — na próxima aula), mas também pode simplesmente não serializar o campo manualmente se você implementar writeObject.
Exemplo:
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient — não é serializado
// Mas também é possível fazer assim:
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// Não gravamos password!
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// password permanece null
}
}
Atenção:
Se você quiser serializar apenas parte do objeto — simplesmente não escreva os campos supérfluos no stream.
4. Chamando os métodos da superclasse: defaultWriteObject e defaultReadObject
Dentro dos seus métodos writeObject e readObject, quase sempre é preciso chamar defaultWriteObject() e defaultReadObject(). É como apertar “salvar rascunho” antes de adicionar suas próprias anotações.
Esses métodos são responsáveis pela serialização padrão de todos os campos que não são transient nem static da classe atual e da superclasse. Se não forem chamados, esses campos não serão serializados e, na desserialização, ficarão vazios.
Exemplo de comportamento incorreto:
private void writeObject(ObjectOutputStream out) throws IOException {
// out.defaultWriteObject(); // esquecemos de chamar!
out.writeInt(123); // algo seu
}
Nesse caso, os campos padrão simplesmente não serão salvos!
5. Prática: log do processo de serialização
Vamos adicionar logging à nossa classe de usuário para ver quando ocorrem a serialização e a desserialização.
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("[LOG] Serialização de Person: " + name + ", idade " + age);
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
System.out.println("[LOG] Desserialização de Person: " + name + ", idade " + age);
}
}
Uso:
Person p = new Person("Bob", 30);
// Salva em arquivo
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
out.writeObject(p);
}
// Carrega do arquivo
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"))) {
Person loaded = (Person) in.readObject();
}
Resultado:
No console, você verá mensagens indicando que o objeto está sendo serializado e desserializado.
6. Erros comuns ao usar writeObject/readObject
Erro nº 1: defaultWriteObject/defaultReadObject não foi chamado. Se você esquecer de chamar esses métodos, os campos padrão não serão serializados e o objeto, após a desserialização, ficará vazio ou incorreto.
Erro nº 2: Assinatura incorreta dos métodos. Os métodos devem ser estritamente private void writeObject(ObjectOutputStream) e private void readObject(ObjectInputStream). Se você torná-los public/protected ou alterar os parâmetros — eles não serão chamados automaticamente.
Erro nº 3: Exceção no método. Se ocorrer uma exceção em writeObject ou readObject, a serialização ou a desserialização será interrompida e o objeto não será salvo/carregado corretamente.
Erro nº 4: Serialização/desserialização da superclasse esquecida. Se sua classe herda de outra classe serializável, chame obrigatoriamente defaultWriteObject/defaultReadObject, caso contrário os campos da superclasse não serão salvos.
Erro nº 5: Serialização de dados sensíveis. Se você esquecer de excluir senhas ou outros dados privados, eles irão parar no arquivo serializado. Use transient ou não os serialize manualmente.
GO TO FULL VERSION