Oi! Na lição de hoje, falamos sobre serialização e desserialização em Java. Começaremos com um exemplo simples. Digamos que você criou um jogo de computador. Se você cresceu nos anos 90 e se lembra dos consoles de jogos daquela época, provavelmente sabe que eles careciam de algo que consideramos natural hoje - a capacidade de salvar e carregar jogos :) Se não, imagine isso! Receio que hoje um jogo sem essas habilidades estaria condenado! O que significa "salvar" e "carregar" um jogo? Bem, entendemos o significado comum: queremos continuar o jogo de onde paramos. Para fazer isso, criamos uma espécie de "ponto de verificação", que usamos para carregar o jogo. Mas o que isso significa para um programador e não para um jogador casual? A resposta é simples: nós'. Digamos que você esteja jogando com a Espanha no Strategium. Seu jogo tem um estado: quem possui quais territórios, quem tem quantos recursos, quem está em aliança com quem, quem está em guerra com quem e assim por diante. Devemos de alguma forma salvar esta informação, o estado do nosso programa, para restaurá-lo no futuro e continuar o jogo. Pois é exatamente para isso que servem a serialização e a desserealização . A serialização é o processo de armazenar o estado de um objeto em uma sequência de bytes. Desserializaçãoé o processo de restauração de um objeto desses bytes. Qualquer objeto Java pode ser convertido em uma sequência de bytes. Por que precisaríamos disso? Dissemos mais de uma vez que os programas não existem por conta própria. Na maioria das vezes, eles interagem com outros programas, trocam dados etc. E uma sequência de bytes é um formato conveniente e eficiente. Por exemplo, podemos transformar nosso
SavedGame
objeto em uma sequência de bytes, enviar esses bytes pela rede para outro computador e, no segundo computador, transformar esses bytes de volta em um objeto Java! Parece difícil, certo? E implementar esse processo parece uma dor de cabeça :/ Felizmente, não é assim! :) Em Java, oSerializable
interface é responsável pelo processo de serialização. Essa interface é extremamente simples: você não precisa implementar um único método para usá-la! É assim que nossa classe de salvamento de jogo parece simples:
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) +
'}';
}
}
As três matrizes são responsáveis por informações sobre territórios, recursos e diplomacia. A interface Serializable informa à máquina virtual Java: " Está tudo bem — se necessário, os objetos desta classe podem ser serializados ". Uma interface sem uma única interface parece estranha :/ Por que é necessário? A resposta para essa pergunta pode ser vista acima: serve apenas para fornecer as informações necessárias à máquina virtual Java. Em uma de nossas lições anteriores, mencionamos brevemente as interfaces de marcadores . Essas 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 você precise implementar.Serializable
é uma dessas interfaces. Outro ponto importante: Por que precisamos da private static final long serialVersionUID
variável que definimos na classe? Por que é necessário? Este campo contém um identificador exclusivo para a versão da classe serializada . Qualquer classe que implementa a Serializable
interface possui um version
identificador. É calculado com base no conteúdo da classe: seus campos, a ordem em que são declarados, métodos, etc. Se mudarmos o tipo de campo e/ou o número de campos em nossa classe, o identificador de versão muda imediatamente . serialVersionUID
também é escrito quando a classe é serializada. Quando tentamos desserializar, ou seja, restaurar um objeto a partir de um conjunto de bytes, o associado serialVersionUID
é comparado com o valor deserialVersionUID
para a classe em nosso programa. Se os valores não corresponderem, um java.io. InvalidClassException será lançada. Veremos um exemplo disso a seguir. Para evitar isso, simplesmente definimos o identificador de versão manualmente em nossa classe. No nosso caso, será simplesmente igual a 1 (mas você pode substituir por qualquer outro número que desejar). Bem, é hora de tentar serializar nosso SavedGame
objeto e 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 neutral position"};
SavedGame savedGame = new SavedGame(territoryInfo, resourcesInfo, diplomacyInfo);
// Create 2 streams 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 the game to a file
objectOutputStream.writeObject(savedGame);
// Close the stream and free resources
objectOutputStream.close();
}
}
Como você pode ver, criamos 2 streams: FileOutputStream
e ObjectOutputStream
. O primeiro pode gravar dados em um arquivo e o segundo converte objetos em bytes. Você já viu construções "aninhadas" semelhantes, por exemplo, new BufferedReader(new InputStreamReader(...))
, em lições anteriores, então elas não devem assustá-lo :) Ao criar uma "cadeia" de dois fluxos, realizamos ambas as tarefas: convertemos o SavedGame
objeto em um conjunto de bytes e salve-o em um arquivo usando o writeObject()
método. E, aliás, nem olhamos o que conseguimos! É hora de olhar para o arquivo! *Nota: você não precisa criar o arquivo com antecedência. Caso não exista um arquivo com esse nome, ele será criado automaticamente* E aqui está o seu conteúdo!
¬н sr SavedGame [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранция воюет СЃ Россией, Рспания заняла позицию нейтралитетаuq ~ t "РЈ Рспании 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Рспании 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 провинций
Uh-oh :( Parece que nosso programa não funcionou :( Na verdade, funcionou. Você se lembra que enviamos um conjunto de bytes, não apenas um objeto ou texto, para o arquivo? Bem, é isso que conjunto de bytes se parece :) Este é o nosso jogo salvo! Se quisermos restaurar nosso objeto original, ou seja, iniciar e continuar o jogo de onde paramos, então precisamos do processo inverso: desserialização . Aqui está o que parecerá em 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=["Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces], resourcesInfo=[Spain has 100 gold, Russia has 80 gold, France has 90 gold], diplomacyInfo=[France is at war with Russia, Spain has taken a neutral position]}
Excelente! Conseguimos primeiro salvar o estado do nosso jogo em um arquivo e depois restaurá-lo a partir do arquivo. Agora vamos tentar fazer a mesma coisa, mas sem o identificador de versão da nossa SavedGame
classe. Não vamos reescrever nossas duas classes. O código deles permanecerá o mesmo, mas removeremos private static final long serialVersionUID
da SavedGame
classe. Aqui está o nosso objeto após a serialização:
¬н sr SavedGameі€MіuОm‰ [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранция воюет СЃ Россией, Рспания заняла позицию нейтралитетаuq ~ t "РЈ Рспании 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Рспании 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 провинций
Mas veja o que acontece quando tentamos desserializá-lo:
InvalidClassException: local class incompatible: stream classdesc serialVersionUID = -196410440475012755, local class serialVersionUID = -6675950253085108747
Esta é exatamente a exceção que mencionamos acima. A propósito, perdemos algo importante. Faz sentido que Strings e primitivos possam ser facilmente serializados: Java provavelmente tem algum tipo de mecanismo interno para fazer isso. Mas e se nossa serializable
classe tiver campos que não são primitivos, mas referências a outros objetos? Por exemplo, vamos criar classes TerritoriesInfo
, ResourcesInfo
e separadas DiplomacyInfo
para trabalhar com nossa SavedGame
classe.
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 + '\'' +
'}';
}
}
E agora surge uma pergunta: todas essas classes precisam ser Serializable
se quisermos serializar nossa SavedGame
classe alterada?
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 testar! Vamos deixar tudo como está e tentar serializar um SavedGame
objeto:
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
TerritoryInfo territoryInfo = new TerritoryInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
ResourceInfo resourceInfo = new ResourceInfo("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 neutral position");
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:
Exception in thread "main" java.io.NotSerializableException: DiplomacyInfo
Não funcionou! Basicamente, essa é a resposta à nossa pergunta. Quando um objeto é serializado, todos os objetos referenciados por suas variáveis de instância são serializados. E se esses objetos também fizerem referência a outros objetos, eles também serão serializados. E assim por diante ad infinitum. Todas as classes desta cadeia devem serSerializable
, caso contrário, será impossível serializá-las e uma exceção será lançada. A propósito, isso pode criar problemas no futuro. O que devemos fazer se, por exemplo, não precisarmos de parte de uma classe quando serializarmos? Ou, por exemplo, e se a TerritoryInfo
classe viesse até nós como parte de alguma biblioteca de terceiros. E suponha ainda que não seja Serializable
e, portanto, não podemos mudá-lo. Acontece que não podemos adicionar um TerritoryInfo
campo ao nossoSavedGame
class, porque isso tornaria toda a SavedGame
classe não serializável! Isso é um problema :/ Em Java, problemas desse tipo são resolvidos usando a transient
palavra-chave. Se você adicionar essa palavra-chave a um campo de sua classe, esse campo não será serializado. Vamos tentar tornar SavedGame
transitório um dos campos de instância da classe. Em seguida, serializaremos e restauraremos um objeto.
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
TerritoryInfo territoryInfo = new TerritoryInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
ResourceInfo resourceInfo = new ResourceInfo("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 neutral position");
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='Spain has 100 gold, Russia has 80 gold, France has 90 gold'}, diplomacyInfo=DiplomacyInfo{info='France is at war with Russia, Spain has taken a neutral position'}}
Além disso, obtivemos uma resposta para nossa pergunta sobre qual valor é atribuído a um transient
campo. Ele recebe o valor padrão. Para objetos, isso é null
. Você pode ler este excelente artigo sobre serialização quando tiver alguns minutos de sobra. Ele também menciona a Externalizable
interface, sobre a qual falaremos na próxima lição. Além disso, o livro "Head-First Java" tem um capítulo sobre este tópico. Dê-lhe alguma atenção :)
GO TO FULL VERSION