JavaRush /Blogue Java /Random-PT /Serialização e desserialização em Java

Serialização e desserialização em Java

Publicado no grupo Random-PT
Olá! Na palestra de hoje falaremos sobre serialização e desserialização em Java. Vamos começar com um exemplo simples. Digamos que você seja o criador de um jogo de computador. Se você cresceu nos anos 90 e se lembra dos consoles de jogos daquela época, provavelmente sabe que hoje faltava algo óbvio - salvar e carregar jogos :) Se não... imagine! Receio que hoje um jogo sem essa oportunidade esteja fadado ao fracasso! E, na verdade, o que é “salvar” e “carregar” um jogo? Bem, no sentido usual, entendemos o que é: queremos continuar o jogo de onde paramos da última vez. Para isso, criamos uma espécie de “checkpoint”, que depois utilizamos para carregar o jogo. Mas o que isso significa, não no sentido cotidiano, mas no sentido de “programador”? A resposta é simples: salvamos o estado do nosso programa. Digamos que você esteja jogando um jogo de estratégia pela Espanha. Seu jogo tem um estado: quem possui quais territórios, quem tem quantos recursos, quem está aliado a quem e quem, pelo contrário, está em guerra e assim por diante. Esta informação, o estado do nosso programa, deve ser salva de alguma forma para posteriormente restaurar os dados e continuar o jogo. É exatamente para isso que são usados ​​os mecanismos de serialização e desserialização . Serialização é o processo de armazenar o estado de um objeto em uma sequência de bytes. A desserialização é o processo de reconstrução de um objeto a partir desses bytes. Qualquer objeto Java é convertido em uma sequência de bytes. Para que serve? Já dissemos mais de uma vez que os programas não existem por si só. Na maioria das vezes eles interagem entre si, trocam dados, etc. E o formato de bytes é conveniente e eficiente para isso. Podemos, por exemplo, transformar nosso objeto de classe SavedGame(um jogo salvo) em uma sequência de bytes, transferir esses bytes pela rede para outro computador e, no segundo computador, transformar esses bytes novamente em um objeto Java! É difícil ouvir, certo? Aparentemente, organizar esse processo não será fácil :/ Felizmente não! :) Em Java, a interface Serializable é responsável pelos processos de serialização . Esta interface é extremamente simples: você não precisa implementar um único método para utilizá-la! Esta é a aparência da nossa classe de jogo salvo:
import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private static final long serialVersionUID = 1L;

   private String[] territoriesInfo;
   private String[] resourcesInfo;
   private String[] diplomacyInfo;

   public SavedGame(String[] territoriesInfo, String[] resourcesInfo, String[] diplomacyInfo){
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public String[] getTerritoriesInfo() {
       return territoriesInfo;
   }

   public void setTerritoriesInfo(String[] territoriesInfo) {
       this.territoriesInfo = territoriesInfo;
   }

   public String[] getResourcesInfo() {
       return resourcesInfo;
   }

   public void setResourcesInfo(String[] resourcesInfo) {
       this.resourcesInfo = resourcesInfo;
   }

   public String[] getDiplomacyInfo() {
       return diplomacyInfo;
   }

   public void setDiplomacyInfo(String[] diplomacyInfo) {
       this.diplomacyInfo = diplomacyInfo;
   }

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoriesInfo=" + Arrays.toString(territoriesInfo) +
               ", resourcesInfo=" + Arrays.toString(resourcesInfo) +
               ", diplomacyInfo=" + Arrays.toString(diplomacyInfo) +
               '}';
   }
}
Três conjuntos de dados são responsáveis ​​por informações sobre territórios, economia e diplomacia, e a interface Serializable informa à máquina Java: “ está tudo bem, se alguma coisa, objetos desta classe podem ser serializados ”. Uma interface que não possui nenhum método parece estranha:/Por que é necessária? A resposta a esta pergunta está acima: apenas para fornecer as informações necessárias à máquina Java. Em uma das palestras anteriores, mencionamos brevemente as interfaces de marcadores. Estas são interfaces informativas especiais que simplesmente marcam nossas classes com informações adicionais que serão úteis para a máquina Java no futuro. Eles não têm nenhum método que precise ser implementado. Portanto, Serializable é uma dessas interfaces. Outro ponto importante: a variável private static final long serialVersionUIDque definimos na classe. Por que é necessário? Este campo contém o identificador exclusivo da versão da classe serializada . Qualquer classe que implemente a interface Serializable possui um identificador de versão. É calculado com base no conteúdo da classe - campos, ordem de declaração, métodos. E se alterarmos o tipo de campo e/ou número de campos em nossa classe, o identificador da versão mudará instantaneamente. serialVersionUID também é escrito quando a classe é serializada. Quando tentamos desserializar, ou seja, restaurar um objeto de um conjunto de bytes, o valor serialVersionUIDé comparado com o valor serialVersionUIDda classe em nosso programa. Se os valores não corresponderem, uma java.io.InvalidClassException será lançada. Veremos um exemplo disso a seguir. Para evitar tais situações, simplesmente definimos manualmente este ID de versão para nossa classe. No nosso caso, será simplesmente igual a 1 (você pode substituir qualquer outro número que desejar). Bem, é hora de tentar serializar nosso objeto SavedGamee ver o que acontece!
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // create our object
       String[] territoryInfo = {"Spain has 6 provinces", "Russia has 10 provinces", "France has 8 provinces"};
       String[] resourcesInfo = {"Spain has 100 gold", "Russia has 80 gold", "France has 90 gold"};
       String[] diplomacyInfo = {"France is at war with Russia, Spain has taken a position of neutrality"};

       SavedGame savedGame = new SavedGame(territoryInfo, resourcesInfo, diplomacyInfo);

       //create 2 threads to serialize the object and save it to a file
       FileOutputStream outputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

       // save game to file
       objectOutputStream.writeObject(savedGame);

       // close the stream and release resources
       objectOutputStream.close();
   }
}
Como você pode ver, criamos 2 threads - FileOutputStreame ObjectOutputStream. O primeiro deles pode gravar dados em um arquivo e o segundo pode converter objetos em bytes. Você já viu construções “aninhadas” semelhantes, por exemplo, new BufferedReader(new InputStreamReader(...))em palestras anteriores, então elas não devem assustá-lo :) Ao criar essa “cadeia” de dois threads, realizamos ambas as tarefas: transformamos o objeto SavedGameem um conjunto de bytes e salve-o em um arquivo usando o método writeObject(). E, aliás, nem verificamos o que conseguimos! É hora de olhar o arquivo! *Nota: o arquivo não precisa ser criado previamente. Se não existir um arquivo com esse nome, ele será criado automaticamente* E aqui está o seu conteúdo! ¬н sr SavedGame [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territóriosInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранцвоюет SЃ R РѕСЃСЃРё ей, Р˜С ЃРїР° РЅРёСЏ Р·Р°Ряла позицию нейтралитетаuq ~ t "РЈ Р˜С ЃРїР°РЅРёРё 100 золотаt Р J R РѕСЃСЃРёРё 80 золотаt! ЃРїР°РЅРёРё 6 РїСЂРѕРІРёРЅС †РёР№t %РЈ Р РѕСЃСЃРёРё 10 etapas †РёР№t &РЈ ФранцРеРё 8 проввинций Ops :( Parece que nosso programa não funcionou :( Na verdade, funcionou. Você se lembra que transferimos exatamente um conjunto de por tes para o arquivo e não apenas um objeto ou texto? Bem, este conjunto é assim :) Este é o nosso jogo salvo! Se quisermos restaurar nosso objeto original, ou seja, carregar e continuar o jogo de onde paramos, precisamos do processo reverso , desserialização ... É assim que ficará no nosso caso:
import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

       SavedGame savedGame = (SavedGame) objectInputStream.readObject();

       System.out.println(savedGame);
   }
}
E aqui está o resultado! SavedGame{territoriesInfo=[A Espanha tem 6 províncias, a Rússia tem 10 províncias, a França tem 8 províncias], resourcesInfo=[A Espanha tem 100 ouro, a Rússia tem 80 ouro, a França tem 90 ouro], diplomacyInfo=[A França está em guerra com a Rússia, A Espanha ocupou uma posição de neutralidade]} Ótimo! Conseguimos primeiro salvar o estado do nosso jogo em um arquivo e depois restaurá-lo a partir do arquivo. Agora vamos tentar fazer o mesmo, mas remova SavedGameo identificador de versão da nossa classe. Não reescreveremos ambas as nossas classes, o código nelas será o mesmo, apenas SavedGameremoveremos private static final long serialVersionUID. Aqui está nosso objeto após a serialização: ¬н sr SavedGameі€MіuОm‰ [ diplomacyInfot [Ljava / lang / String; РµС ‚ СЃ Россией, Р˜СЃРїР°РЅРЏР·Р°РЅСЏР»Р° RїРѕР·РёС†РёСЋ РЅРµР№С ‚ралитетаuq ~ t "РЈ Р˜СЃРї ании 100 Р · олотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 Р·Рѕ лотаuq ~ t &РЈ Р˜СЃРїР°РЅРёРё 6 R їСЂРѕРІРёРЅС† РёР№t %РЈ Р РѕСЃСЃРёРё 10 провнинцийt &РЈ Р ¤СЂР°РЅС†РёРё 8 проввинцинА E ao tentar desserializá-lo, foi o que aconteceu: InvalidClassException: classe local incompatível: stream classdesc serialVersionUID = - 196410440475012755, classe local serialVersionUID = -6675950253085108747 Esta é a mesma exceção mencionada acima. Você pode ler mais sobre isso no artigo de um de nossos alunos. A propósito, perdemos um ponto importante. É claro que strings e primitivas são facilmente serializados: Java certamente tem alguns mecanismos internos para isso. Mas e se nossa serializableclasse tiver campos que são expressos não como primitivos, mas como referências a outros objetos? Vamos, por exemplo, criar classes separadas TerritoriesInfopara trabalhar com a nossa classe . ResourcesInfoDiplomacyInfoSavedGame
public class TerritoriesInfo {

   private String info;

   public TerritoriesInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "TerritoriesInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}

public class ResourcesInfo {

   private String info;

   public ResourcesInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "ResourcesInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}

public class DiplomacyInfo {

   private String info;

   public DiplomacyInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "DiplomacyInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}
Mas agora temos uma pergunta: todas essas classes deveriam ser Serializable se quisermos serializar a classe alterada SavedGame?
import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private TerritoriesInfo territoriesInfo;
   private ResourcesInfo resourcesInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public TerritoriesInfo getTerritoriesInfo() {
       return territoriesInfo;
   }

   public void setTerritoriesInfo(TerritoriesInfo territoriesInfo) {
       this.territoriesInfo = territoriesInfo;
   }

   public ResourcesInfo getResourcesInfo() {
       return resourcesInfo;
   }

   public void setResourcesInfo(ResourcesInfo resourcesInfo) {
       this.resourcesInfo = resourcesInfo;
   }

   public DiplomacyInfo getDiplomacyInfo() {
       return diplomacyInfo;
   }

   public void setDiplomacyInfo(DiplomacyInfo diplomacyInfo) {
       this.diplomacyInfo = diplomacyInfo;
   }

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoriesInfo=" + territoriesInfo +
               ", resourcesInfo=" + resourcesInfo +
               ", diplomacyInfo=" + diplomacyInfo +
               '}';
   }
}
Bem, vamos verificar isso na prática! Vamos deixar tudo como está por enquanto e tentar serializar o objeto SavedGame:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // create our object
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourcesInfo resourcesInfo = new ResourcesInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("France is at war with Russia, Spain has taken a position of neutrality");


       SavedGame savedGame = new SavedGame(territoriesInfo, resourcesInfo, diplomacyInfo);

       FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

       objectOutputStream.writeObject(savedGame);

       objectOutputStream.close();
   }
}
Resultado: Exceção no thread "main" java.io.NotSerializableException: DiplomacyInfo Failed! Na verdade, aqui está a resposta à nossa pergunta. Quando você serializa um objeto, todos os objetos aos quais ele faz referência em suas variáveis ​​de instância são serializados. E se esses objetos também fizerem referência a terceiros objetos, eles também serão serializados. E assim por diante, ad infinitum. Todas as classes nesta cadeia devem ser serializáveis, caso contrário não serão serializáveis ​​e uma exceção será lançada. A propósito, isso pode criar problemas no futuro. O que devemos fazer, por exemplo, se não precisarmos de parte da classe durante a serialização? Ou, por exemplo, TerritoryInfoherdamos uma classe em nosso programa como parte de alguma biblioteca. No entanto, não é serializável e, portanto, não podemos alterá-lo. Acontece que não podemos adicionar um campo TerritoryInfoà nossa classe , porque então a classe inteira se tornará não serializável! Problema:/ Problemas deste tipo são resolvidos em Java usando a palavra-chave . Se você adicionar esta palavra-chave a um campo de classe, o valor deste campo não será serializado. Vamos tentar fazer um dos campos da nossa classe , após o qual serializaremos e restauraremos um objeto. SavedGameSavedGameSerialização e desserialização em Java - 2transientSavedGame transient
import java.io.Serializable;

public class SavedGame implements Serializable {

   private transient TerritoriesInfo territoriesInfo;
   private ResourcesInfo resourcesInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   //...getters, setters, toString()...
}



import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // create our object
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourcesInfo resourcesInfo = new ResourcesInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("France is at war with Russia, Spain has taken a position of neutrality");


       SavedGame savedGame = new SavedGame(territoriesInfo, resourcesInfo, diplomacyInfo);

       FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

       objectOutputStream.writeObject(savedGame);

       objectOutputStream.close();
   }
}


import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

       SavedGame savedGame = (SavedGame) objectInputStream.readObject();

       System.out.println(savedGame);

       objectInputStream.close();


   }
}
E aqui está o resultado: SavedGame{territoriesInfo=null, resourcesInfo=ResourcesInfo{info='A Espanha tem 100 ouro, a Rússia tem 80 ouro, a França tem 90 ouro'}, diplomacyInfo=DiplomacyInfo{info='A França está em guerra com a Rússia, A Espanha assumiu uma posição de neutralidade'}} Ao mesmo tempo, recebemos uma resposta à questão de qual valor será atribuído transientao campo -. É atribuído um valor padrão. No caso de objetos, isso é null. Quando quiser, você pode ler este excelente artigo sobre serialização . Também fala sobre a interface Externalizable, da qual falaremos na próxima palestra. Além disso, há um capítulo sobre esse assunto no livro “Head-First Java”, preste atenção nele :)
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION