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