Serialização em Java. Não é tão simples



A serialização é um processo que converte um objeto em uma sequência de bytes, a partir da qual ele pode ser completamente restaurado. Por que isso é necessário? O fato é que, durante a execução normal do programa, a vida útil máxima de qualquer objeto é conhecida - desde o início do programa até o final. A serialização permite expandir essa estrutura e "dar vida" ao objeto da mesma maneira entre os lançamentos do programa.

Um bônus adicional a tudo é a preservação da plataforma cruzada. Não importa qual sistema operacional você tenha, a serialização converte o objeto em um fluxo de bytes, que pode ser restaurado em qualquer sistema operacional. Se você precisar transferir um objeto pela rede, poderá serializá-lo, salvá-lo em um arquivo e transferi-lo pela rede para o destinatário. Ele poderá recuperar o objeto recebido. A serialização também permite que você chame remotamente métodos (Java RMI) que estão em máquinas diferentes com, possivelmente, sistemas operacionais diferentes, e trabalhe com eles como se estivessem na máquina do processo java de chamada.

A implementação de um mecanismo de serialização é bastante simples. Sua classe precisa implementar a interface serializável . Essa interface é um identificador que não possui métodos, mas informa à jvm que objetos dessa classe podem ser serializados. Como o mecanismo de serialização está conectado ao sistema básico de entrada / saída e converte o objeto em um fluxo de bytes, para executá-lo, é necessário criar um fluxo de saída OutputStream , empacotá-lo no ObjectOutputStream e chamar o método writeObject (). Para restaurar um objeto, você precisa compactar um InputStream em um ObjectInputStream e chamar o método readObject ().

Durante o processo de serialização, junto com o objeto serializável, seu gráfico de objetos é salvo. I.e. todos os objetos relacionados a isso, objetos de outras classes também serão serializados com ele.

Considere um exemplo de serialização de um objeto da classe Person.

import java.io.*; class Home implements Serializable { private String home; public Home(String home) { this.home = home; } public String getHome() { return home; } } public class Person implements Serializable { private String name; private int countOfNiva; private String fatherName; private Home home; public Person(String name, int countOfNiva, String fatherName, Home home) { this.name = name; this.countOfNiva = countOfNiva; this.fatherName = fatherName; this.home = home; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", countOfNiva=" + countOfNiva + ", fatherName='" + fatherName + '\'' + ", home=" + home + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Home home = new Home("Vishnevaia 1"); Person igor = new Person("Igor", 2, "Raphael", home); Person renat = new Person("Renat", 2, "Raphael", home); //      ObjectOutputStream ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("person.out")); objectOutputStream.writeObject(igor); objectOutputStream.writeObject(renat); objectOutputStream.close(); //       ObjectInputStream ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream("person.out")); Person igorRestored = (Person) objectInputStream.readObject(); Person renatRestored = (Person) objectInputStream.readObject(); objectInputStream.close(); //    ByteArrayOutputStream ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream2 = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream2.writeObject(igor); objectOutputStream2.writeObject(renat); objectOutputStream2.flush(); //    ByteArrayInputStream ObjectInputStream objectInputStream2 = new ObjectInputStream( new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); Person igorRestoredFromByte = (Person) objectInputStream2.readObject(); Person renatRestoredFromByte = (Person) objectInputStream2.readObject(); objectInputStream2.close(); System.out.println("Before Serialize: " + "\n" + igor + "\n" + renat); System.out.println("After Restored From Byte: " + "\n" + igorRestoredFromByte + "\n" + renatRestoredFromByte); System.out.println("After Restored: " + "\n" + igorRestored + "\n" + renatRestored); } } 

Conclusão:

 Before Serialize: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@355da254} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@355da254} After Restored From Byte: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} After Restored: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} 

Neste exemplo, a classe Home é criada para demonstrar que, ao serializar um objeto Person , o gráfico de seus objetos é serializado com ele. A classe Home também deve implementar a interface Serializable , caso contrário, uma java.io.NotSerializableException ocorrerá. O exemplo também descreve a serialização usando a classe ByteArrayOutputStream .

Uma conclusão interessante pode ser tirada dos resultados da execução do programa: ao restaurar objetos que tinham uma referência ao mesmo objeto antes da serialização, esse objeto será restaurado apenas uma vez . Isso pode ser visto nos mesmos links nos objetos após a recuperação:

 After Restored From Byte: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} After Restored: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} 

No entanto, também é visto que, ao gravar com dois fluxos de saída (temos ObjectInputStream e ByteArrayOutputStream ), o objeto inicial será recriado, apesar do fato de já ter sido criado anteriormente em um dos fluxos. Vemos isso em diferentes endereços de objetos domésticos recebidos em dois fluxos. Acontece que se você serializar com um fluxo de saída e depois restaurar o objeto, temos a garantia de restaurar a rede completa de objetos sem duplicatas desnecessárias. Obviamente, durante a execução do programa, o estado dos objetos pode mudar, mas isso está na consciência do programador.

O problema

O exemplo também mostra que, ao restaurar um objeto, pode ocorrer uma ClassNotFoundException . Qual o motivo disso? O fato é que podemos facilmente serializar um objeto da classe Person em um arquivo, transferi-lo pela rede para nosso amigo, que pode restaurar o objeto por outro aplicativo no qual a classe Person simplesmente não existe.

Sua serialização. Como fazer?

E se você quiser gerenciar a serialização? Por exemplo, seu objeto armazena o nome de usuário e a senha dos usuários. Você precisa serializá-lo para transmissão adicional pela rede. Passar uma senha nesse caso é extremamente não confiável. Como resolver este problema? Existem duas maneiras. Primeiro, use a palavra-chave transitória . Segundo, em vez de perceber o interesse serializável , use sua extensão - a interface Externalizável . Considere os exemplos do primeiro e do segundo métodos para compará-los.

A primeira maneira - serialização usando transiente

 import java.io.*; public class Logon implements Serializable { private String login; private transient String password; public Logon(String login, String password) { this.login = login; this.password = password; } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon igor = new Logon("IgorIvanovich", "Khoziain"); Logon renat = new Logon("Renat", "2500RUB"); System.out.println("Before: \n" + igor); System.out.println(renat); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out")); out.writeObject(igor); out.writeObject(renat); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out")); igor = (Logon) in.readObject(); renat = (Logon) in.readObject(); System.out.println("After: \n" + igor); System.out.println(renat); } } 

Conclusão:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} After: Logon{login='IgorIvanovich', password='null'} Logon{login='Renat', password='null'} 

A segunda maneira - serialização com a implementação da interface Externalizável

 import java.io.*; public class Logon implements Externalizable { private String login; private String password; public Logon() { } public Logon(String login, String password) { this.login = login; this.password = password; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(login); } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { login = (String) in.readObject(); } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon igor = new Logon("IgorIvanovich", "Khoziain"); Logon renat = new Logon("Renat", "2500RUB"); System.out.println("Before: \n" + igor); System.out.println(renat); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out")); out.writeObject(igor); out.writeObject(renat); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out")); igor = (Logon) in.readObject(); renat = (Logon) in.readObject(); System.out.println("After: \n" + igor); System.out.println(renat); } } 

Conclusão:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} After: Logon{login='IgorIvanovich', password='null'} Logon{login='Renat', password='null'} 

A primeira diferença entre as duas opções que chama sua atenção é o tamanho do código. Ao implementar a interface Externalizável , precisamos substituir dois métodos: writeExternal () e readExternal () . No método writeExternal () , indicamos quais campos serão serializados e como, em readExternal (), como lê-los. Ao usar a palavra transitório, indicamos explicitamente quais campos ou campos não precisam ser serializados. Também observamos que no segundo método, criamos explicitamente um construtor padrão, além disso, um construtor público. Por que isso é feito? Vamos tentar executar o código sem esse construtor. E veja a saída:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} Exception in thread "main" java.io.InvalidClassException: Logon; no valid constructor at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169) at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2043) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431) at Logon.main(Logon.java:45) 

Temos a exceção java.io.InvalidClassException . Qual o motivo disso? Se você seguir o rastreamento da pilha, poderá descobrir que existem linhas no construtor da classe ObjectStreamClass :

  if (externalizable) { cons = getExternalizableConstructor(cl); } else { cons = getSerializableConstructor(cl); 

Para a interface Externalizable , o método construtor getExternalizableConstructor () será chamado , dentro do qual, através do Reflection , tentaremos obter o construtor padrão da classe para a qual estamos restaurando o objeto. Se não conseguimos encontrá-lo ou não é público , obtemos uma exceção. Você pode contornar essa situação da seguinte maneira: não crie explicitamente nenhum construtor na classe e preencha os campos usando setters e obtenha o valor com getters. Em seguida, ao compilar a classe, um construtor padrão será criado, que estará disponível para getExternalizableConstructor () . Para Serializable, o método getSerializableConstructor () obtém o construtor da classe Object e procura a classe desejada, se não a encontrar, obtemos uma exceção ClassNotFoundException . Acontece que a principal diferença entre Serializable e Externalizable é que o primeiro não precisa de um construtor para criar a recuperação do objeto. Ele simplesmente se recuperará completamente dos bytes. No segundo, durante a restauração, um objeto será criado primeiro usando o construtor no ponto de declaração e, em seguida, os valores de seus campos de bytes recebidos durante a serialização serão gravados nele. Pessoalmente, eu prefiro o primeiro método, é muito mais simples. Além disso, mesmo se ainda precisarmos definir o comportamento da serialização, não podemos usar Externalizable , nem implementar Serializable adicionando (sem substituir) os métodos writeObject () e readObject () . Mas, para que eles “trabalhem”, sua assinatura deve ser rigorosamente observada.

 import java.io.*; public class Talda implements Serializable { private String name; private String description; public Talda(String name, String description) { this.name = name; this.description = description; } private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); System.out.println("Our writeObject"); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); System.out.println("Our readObject"); } @Override public String toString() { return "Talda{" + "name='" + name + '\'' + ", description='" + description + '\'' + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Talda partizanka = new Talda("Partizanka", "Viiiski"); System.out.println("Before: \n" + partizanka); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Talda.out")); out.writeObject(partizanka); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Talda.out")); partizanka = (Talda) in.readObject(); System.out.println("After: \n" + partizanka); } } 

Conclusão:

 Before: Talda{name='Partizanka', description='Viiiski'} Our writeObject Our readObject After: Talda{name='Partizanka', description='Viiiski'} 

Dentro de nossos métodos adicionados, defaultWriteObject () e defaultReadObject () são chamados. Eles são responsáveis ​​pela serialização padrão, como se funcionasse sem os métodos que adicionamos.

De fato, esta é apenas a ponta do iceberg; se você continuar a se aprofundar no mecanismo de serialização, então com um alto grau de probabilidade, poderá encontrar mais nuances, descobrindo o que dizemos: "Serialização ... não é tão simples".

Source: https://habr.com/ru/post/pt431524/


All Articles