serialization

A serialização de objetos em Java é um recurso muito importante e útil em aplicações específicas. Diversas APIs do Java beneficiam-se dela, por exemplo, para chamadas de métodos remotos (RMI) e migração de sessões em Servlets de aplicações web clusterizadas.

Serializar um objeto consiste em converter os valores de uma instância em uma sequência (stream) de bytes (dados binários) de forma que o estado do objeto possa ser posteriormente recuperado.

Tornar uma classe serializável em Java é muito simples: basta implementar a interface java.io.Serializable.

Porém, nem sempre os detalhes são tão óbvios. E quanto a atributos herdados? O que ocorre se a superclasse não for serializável?

Tratando-se de herança, existem algumas nuances quanto ao que será ou não incluído na serialização. O ObjectOutputStream irá serializar todas as classes da hierarquia que são marcados com java.io.Serializable e seus descendentes. Desses, os atributos não estáticos, não transientes e que também são marcados com a referida interface serão serializados.

Meio complicado, não? Vamos ver um…

Exemplo prático (com erro)

Primeiro, duas classes que serão referenciadas, uma serializável e outra não:

class OutraClasseSerializavel implements Serializable {
    int outroValorSerializavel;
}

class OutraClasse {
    int outroValor;
}

Segundo, uma classe “pai” e uma “filha”:

class Pai {
    OutraClasse outraClassePai;
    OutraClasseSerializavel outraClasseSerializavelPai;
    int valorPai;
}

class Filha extends Pai implements Serializable {
    OutraClasse outraClasseFilha;
    OutraClasseSerializavel outraClasseSerializavelFilha;        
    int valorFilha;
}

Note que as duas classes possuem valores e referências para classes serializáveis e não serializáveis.

O que acontece se tentarmos serializar a classe Filha? Ocorre um java.io.NotSerializableException por causa da referência à classe não serializável OutraClasse na classe Filha.

Exemplo prático

Se removermos a referência à classe não serializável da classe Filha, o erro não ocorre:

class Filha extends Pai implements Serializable {
    OutraClasseSerializavel outraClasseSerializavelFilha;        
    int valorFilha;
}

Testando e analisando o resultado

Vamos fazer um teste:

Filha filha = new Filha();

//valores da classe filha
filha.valorFilha = 11;
filha.outraClasseSerializavelFilha = new OutraClasseSerializavel();
filha.outraClasseSerializavelFilha.outroValorSerializavel = 33;

//valores da classe pai
filha.valorPai = 22;
filha.outraClasseSerializavelPai = new OutraClasseSerializavel();
filha.outraClasseSerializavelPai.outroValorSerializavel = 44;
filha.outraClassePai = new OutraClasse();
filha.outraClassePai.outroValor = 55;

//serializa
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("filha.out")));
oos.writeObject(filha);
oos.close();

//recupera classe serializada
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("filha.out")));
Filha filhaRecuperada = (Filha) ois.readObject();
ois.close();

Finalmente, vamos imprimir e analisar os valores retornados…

Atributo primitivo na classe serializável

System.out.println(filhaRecuperada.valorFilha);

Saída:

11

Obviamente, o atributo valorFilha é devidamente serializado e recuperado porque faz parte da classe serializável e é um tipo primitivo.

Referência à classe serializável em uma classe também serializável

System.out.println(filhaRecuperada.outraClasseSerializavelFilha.outroValorSerializavel);

Saída:

33

O atributo outraClasseSerializavelFilha também foi serializado corretamente, assim como seu valor, porque é uma referência a uma classe serializável a partir da classe Filha que é serializável.

Atributo primitivo na classe Pai, que não é serializável

System.out.println(filhaRecuperada.valorPai);

Saída:

0

Observamos agora que, embora não ocorram erros, atributos estáticos em uma superclasse não serializável não são serializados.

Referência à classes serializáveis e não serializáveis em uma superclasse não serializável

System.out.println(filhaRecuperada.outraClassePai);
System.out.println(filhaRecuperada.outraClasseSerializavelPai);

Saída:

null

null

E, finalmente, observamos que referências a classes de qualquer tipo (serializáveis ou não) em uma superclasse não serializável também serão excluídas da serialização.

Considerações

Estender uma classe para torná-la serializável não funciona, pois como foi visto o processo de serialização ignora as superclasses não serializáveis e um erro ocorre ao incluirmos um atributo não serializável.

Mas existe alguma solução? A resposta é sim!

Solução: readObject e writeObject

A documentação da classe java.io.Serializable aponta alguns métodos que devem ser implementados para que você possa alterar “manualmente” a forma como o Java serializa e desserializa um objeto.

As assinaturas são:

private void writeObject(java.io.ObjectOutputStream out)
    throws IOException
private void readObject(java.io.ObjectInputStream in)
    throws IOException, ClassNotFoundException;

Exemplo de implementação

Segue uma implementação básica dos métodos readObject() e writeObject() na classe Filha que resolvem o problema da serialização tanto do atributo inteiro da superclasse quanto das referências a outros objetos:

class Filha extends Pai implements Serializable {

    int valorFilha;
    transient OutraClasse outraClasseFilha;
    OutraClasseSerializavel outraClasseSerializavelFilha;

    private void readObject(java.io.ObjectInputStream stream)
            throws IOException, ClassNotFoundException {
        valorFilha =  stream.readInt();
        outraClasseFilha = new OutraClasse();
        outraClasseFilha.outroValor = stream.readInt();
        outraClasseSerializavelFilha = (OutraClasseSerializavel) stream.readObject();

        valorPai = stream.readInt();
        outraClassePai = new OutraClasse();
        outraClassePai.outroValor = stream.readInt();
        outraClasseSerializavelPai = (OutraClasseSerializavel) stream.readObject();
    }

    private void writeObject(java.io.ObjectOutputStream stream)
            throws IOException {
        stream.writeInt(valorFilha);
        stream.writeInt(outraClasseFilha.outroValor);
        stream.writeObject(outraClasseSerializavelFilha);

        stream.writeInt(valorPai);
        stream.writeInt(outraClassePai.outroValor);
        stream.writeObject(outraClasseSerializavelPai);
    }

}

Então fazemos um novo teste:

Filha filha = new Filha();

//valores da classe filha
filha.valorFilha = 11;
filha.outraClasseSerializavelFilha = new OutraClasseSerializavel();
filha.outraClasseSerializavelFilha.outroValorSerializavel = 22;
filha.outraClasseFilha = new OutraClasse();
filha.outraClasseFilha.outroValor = 33;

//valores da classe pai
filha.valorPai = 44;
filha.outraClasseSerializavelPai = new OutraClasseSerializavel();
filha.outraClasseSerializavelPai.outroValorSerializavel = 55;
filha.outraClassePai = new OutraClasse();
filha.outraClassePai.outroValor = 66;

//serializa
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("c.out")));
oos.writeObject(filha);
oos.close();

//recupera classe serializada
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("c.out")));
Filha filhaRecuperada = (Filha) ois.readObject();
ois.close();

//valores da classe filha
System.out.println(filhaRecuperada.valorFilha);
System.out.println(filhaRecuperada.outraClasseSerializavelFilha.outroValorSerializavel);
System.out.println(filhaRecuperada.outraClasseFilha.outroValor);

//valores da classe pai
System.out.println(filhaRecuperada.valorPai);
System.out.println(filhaRecuperada.outraClasseSerializavelPai.outroValorSerializavel);
System.out.println(filhaRecuperada.outraClassePai.outroValor);

E obtemos a saída:

11
22
33
44
55
66

Todos os atributos foram salvos!

Conclusão

Embora o Java não resolva toda a questão da serialização automagicamente, ele nos fornece um mecanismo prático e flexível para resolver isso, pois permite controlar completamente como o objeto é salvo e recuperado do arquivo. Por outro lado, isso exige a codificação manual de cada elemento, na ordem correta.

Este artigo é baseado na minha resposta no Stack Overflow em Português

Créditos da imagem do sapo: GridGain Blog