CodeGym /Cursos /JAVA 25 SELF /Controle do processo de serialização: writeObject, readOb...

Controle do processo de serialização: writeObject, readObject

JAVA 25 SELF
Nível 43 , Lição 0
Disponível

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.

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