JavaRush /Blog Java /Random-ES /Serialización y deserialización en Java

Serialización y deserialización en Java

Publicado en el grupo Random-ES
¡Hola! En la conferencia de hoy hablaremos sobre serialización y deserialización en Java. Comencemos con un ejemplo simple. Digamos que eres el creador de un juego de computadora. Si creciste en los años 90 y recuerdas las consolas de juegos de esa época, probablemente sepas que hoy les faltaba algo obvio: guardar y cargar juegos :) Si no... ¡imagínate! ¡Me temo que hoy un juego sin esa opción estará condenado al fracaso! Y, en realidad, ¿qué es “guardar” y “cargar” un juego? Bueno, en el sentido habitual, entendemos lo que es: queremos continuar el juego desde donde lo dejamos la última vez. Para ello, creamos una especie de “punto de control”, que luego utilizamos para cargar el juego. Pero ¿qué significa esto, no en el sentido cotidiano, sino en el sentido de “programador”? La respuesta es sencilla: guardamos el estado de nuestro programa. Digamos que estás jugando un juego de estrategia para España. Tu juego tiene un estado: quién posee qué territorios, quién tiene cuántos recursos, quién está aliado con quién y quién, por el contrario, está en guerra, etc. Esta información, el estado de nuestro programa, debemos guardarla de alguna manera para poder luego restaurar los datos y continuar con el juego. Precisamente para esto se utilizan los mecanismos de serialización y deserialización . La serialización es el proceso de almacenar el estado de un objeto en una secuencia de bytes. La deserialización es el proceso de reconstruir un objeto a partir de estos bytes. Cualquier objeto Java se convierte en una secuencia de bytes. ¿Para qué sirve? Hemos dicho más de una vez que los programas no existen por sí solos. La mayoría de las veces interactúan entre sí, intercambian datos, etc. Y el formato de bytes es conveniente y eficaz para ello. Podemos, por ejemplo, convertir nuestro objeto de clase SavedGame(una partida guardada) en una secuencia de bytes, transferir esos bytes a través de la red a otra computadora y, en la segunda computadora, convertir esos bytes nuevamente en un objeto Java. Es difícil de oír, ¿verdad? Al parecer, organizar este proceso no será fácil :/ ¡Afortunadamente no! :) En Java, la interfaz Serializable es responsable de los procesos de serialización . Esta interfaz es extremadamente simple: ¡no necesitas implementar un solo método para usarla! Así es como se verá nuestra clase de guardar partida:
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) +
               '}';
   }
}
Tres conjuntos de datos son responsables de la información sobre territorios, economía y diplomacia, y la interfaz Serializable le dice a la máquina Java: " todo está bien, en todo caso, los objetos de esta clase se pueden serializar ". Una interfaz que no tiene ningún método parece extraña :/ ¿Por qué es necesaria? La respuesta a esta pregunta está arriba: solo para proporcionar la información necesaria a la máquina Java. En una de las conferencias anteriores, mencionamos brevemente las interfaces de marcadores. Estas son interfaces informativas especiales que simplemente marcan nuestras clases con información adicional que será útil para la máquina Java en el futuro. No tienen ningún método que deba implementarse. Entonces, Serializable es una de esas interfaces. Otro punto importante: la variable private static final long serialVersionUIDque definimos en la clase. ¿Por qué es necesario? Este campo contiene el identificador de versión único de la clase serializada . Cualquier clase que implemente la interfaz Serializable tiene un identificador de versión. Se calcula en función del contenido de la clase: campos, orden de declaración, métodos. Y si cambiamos el tipo de campo y/o la cantidad de campos en nuestra clase, el identificador de versión cambiará instantáneamente. serialVersionUID también se escribe cuando se serializa la clase. Cuando intentamos deserializar, es decir, restaurar un objeto a partir de un conjunto de bytes, el valor serialVersionUIDse compara con el valor serialVersionUIDde la clase en nuestro programa. Si los valores no coinciden, se generará una java.io.InvalidClassException. Veremos un ejemplo de esto a continuación. Para evitar este tipo de situaciones, simplemente configuramos manualmente este ID de versión para nuestra clase. En nuestro caso, simplemente será igual a 1 (puedes sustituirlo por cualquier otro número que desees). Bueno, ¡es hora de intentar serializar nuestro objeto SavedGamey ver qué pasa!
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

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

       // creamos nuestro objeto
       String[] territoryInfo = {"España tiene 6 provincias", "Rusia tiene 10 provincias", "Francia tiene 8 provincias"};
       String[] resourcesInfo = {"España tiene 100 de oro", "Rusia tiene 80 de oro", "Francia tiene 90 de oro"};
       String[] diplomacyInfo = {"Francia está en guerra con Rusia, España ha tomado una posición de neutralidad"};

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

       //crear 2 subprocesos para serializar el objeto y guardarlo en un archivo
       FileOutputStream outputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

       // guarda el juego en un archivo
       objectOutputStream.writeObject(savedGame);

       // cerrar la transmisión y liberar recursos
       objectOutputStream.close();
   }
}
Como puedes ver, hemos creado 2 hilos - FileOutputStreamy ObjectOutputStream. El primero de ellos puede escribir datos en un archivo y el segundo puede convertir objetos en bytes. Ya has visto construcciones "anidadas" similares, por ejemplo, new BufferedReader(new InputStreamReader(...))en conferencias anteriores, por lo que no deberían asustarte :) Al crear una "cadena" de dos hilos, realizamos ambas tareas: convertimos el objeto SavedGameen un conjunto de bytes y guárdelo en un archivo usando el método writeObject(). Y, por cierto, ¡ni siquiera comprobamos lo que teníamos! ¡Es hora de mirar el archivo! *Nota: no es necesario crear el archivo con antelación. Si un archivo con ese nombre no existe, se creará automáticamente* ¡ Y aquí está su contenido! ¬н sr SavedGame [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ TerritoryInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранцвоюРμС‚ SЃ R РѕСЃСЃРёРμР№, Р ˜С ЃРїР° РЅРёСЏ Р·Р°Ряла позицию РЅРμйтралитРμтаuq ~ t "РЈ Р˜СЃРїР° РЅРёРё 100 золотаt Р J R РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Р˜СЃРї ании 6 РїСЂРѕРІРёРЅС †РёР№t %РЈ Р РѕСЃСЃРёРё 10 РїСЂРѕРІРёРЅС † ёр№t & ј р ° р р † р р 8 їїрр †рёрёё † ёр№ OOPS :( Parece que nuestro programa no funcionó :( en realidad, funcionó. Recuerdas que transfirimos exactamente un conjunto de bytes al archivo y y y y y y y y y ¿No es solo un objeto o texto? Bueno, así es como se ve este conjunto :) ¡Esta es nuestra partida guardada! Si queremos restaurar nuestro objeto original, es decir, cargar y continuar el juego desde donde lo dejamos, necesitamos el proceso inverso, deserialización ... Así es como se verá en nuestro 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);
   }
}
¡Y aqui esta el resultado! SavedGame{territoriesInfo=[España tiene 6 provincias, Rusia tiene 10 provincias, Francia tiene 8 provincias], resourcesInfo=[España tiene 100 de oro, Rusia tiene 80 de oro, Francia tiene 90 de oro], diplomacyInfo=[Francia está en guerra con Rusia, España ha ocupado posición de neutralidad]} ¡Genial! Primero logramos guardar el estado de nuestro juego en un archivo y luego restaurarlo desde el archivo. Ahora intentemos hacer lo mismo, pero eliminando SavedGameel identificador de versión de nuestra clase. No reescribiremos nuestras dos clases, el código en ellas será el mismo, simplemente lo SavedGameeliminaremos private static final long serialVersionUID. Aquí está nuestro objeto después de la serialización: ¬н sr SavedGameі€MіuОm‰ [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ TerritoryInfoq ~ xpur [Ljava.lang.String;ТВзй{G xp t pФранция РІРѕСЋРμС ‚ СЃ Р РѕСЃСЃРёРμР№, Р˜СЃРїР°РЅРЏР·Р°РЅСЏР»Р° RїРѕР·РёС†РёСЋ РЅРμйтра литРμтаuq ~ t "РЈ Р˜СЃРї ании 100 Р · олотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Р˜СЃРїР°РЅРёРё 6 Рпровнинцийt &РЈ Франции 8 проввинцинА Y al intentar deserializarlo, esto es lo que sucedió: InvalidClassException: clase local incompatible: stream classdesc serialVersionUID = - 196410440475012755, clase local serialVersionUID = -6675950253085108747 Esta es la misma excepción que se mencionó anteriormente. Puede leer más sobre esto en el artículo de uno de nuestros estudiantes. Por cierto, nos perdimos un punto importante. Está claro que las cadenas y las primitivas se serializan fácilmente: Java probablemente tenga algunos, entonces existen mecanismos integrados para esto. Pero ¿qué pasa si nuestra serializableclase tiene campos que no se expresan como primitivos, sino como referencias a otros objetos? Por ejemplo, creemos clases separadas TerritoriesInfopara trabajar con nuestra clase . 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 + '\'' +
               '}';
   }
}
Pero ahora tenemos una pregunta: ¿deberían todas estas clases ser serializables si queremos serializar la clase modificada 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 +
               '}';
   }
}
Bueno, ¡comprobemos esto en la práctica! Dejemos todo como está por ahora e intentemos serializar el 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 {

       // creamos nuestro objeto
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("España tiene 6 provincias, Rusia tiene 10 provincias, Francia tiene 8 provincias");
       ResourcesInfo resourcesInfo = new ResourcesInfo("España tiene 100 de oro, Rusia tiene 80 de oro, Francia tiene 90 de oro");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("Francia está en guerra con Rusia, España ha tomado una posición de neutralidad");


       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: Excepción en el hilo "principal" java.io.NotSerializableException: ¡ Error en DiplomacyInfo! En realidad, aquí está la respuesta a nuestra pregunta. Cuando serializa un objeto, todos los objetos a los que hace referencia en sus variables de instancia se serializan. Y si esos objetos también hacen referencia a terceros objetos, también se serializan. Y así hasta el infinito. Todas las clases de esta cadena deben ser serializables; de lo contrario, no serán serializables y se generará una excepción. Esto, por cierto, puede crear problemas en el futuro. ¿Qué debemos hacer, por ejemplo, si no necesitamos parte de la clase durante la serialización? O, por ejemplo, TerritoryInfoheredamos una clase en nuestro programa como parte de alguna biblioteca. Sin embargo, no es serializable y, en consecuencia, no podemos cambiarlo. ¡Resulta que no podemos agregar un campo TerritoryInfoa nuestra clase , porque entonces toda la clase dejará de ser serializable! Problema:/ Los problemas de este tipo se resuelven en Java usando la palabra clave . Si agrega esta palabra clave a un campo de clase, el valor de este campo no se serializará. Intentemos crear uno de los campos de nuestra clase , después de lo cual serializaremos y restauraremos un objeto. SavedGameSavedGameSerialización y Deserialización en 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 {

       // creamos nuestro objeto
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("España tiene 6 provincias, Rusia tiene 10 provincias, Francia tiene 8 provincias");
       ResourcesInfo resourcesInfo = new ResourcesInfo("España tiene 100 de oro, Rusia tiene 80 de oro, Francia tiene 90 de oro");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("Francia está en guerra con Rusia, España ha tomado una posición de neutralidad");


       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();


   }
}
Y aquí está el resultado: SavedGame{territoriesInfo=null, resourcesInfo=ResourcesInfo{info='España tiene 100 de oro, Rusia tiene 80 de oro, Francia tiene 90 de oro'}, diplomacyInfo=DiplomacyInfo{info='Francia está en guerra con Rusia, España ha adoptado una posición de neutralidad'}} Al mismo tiempo, recibimos una respuesta a la pregunta de qué valor se le asignará transiental campo. Se le asigna un valor predeterminado. En el caso de los objetos esto es null. En tu tiempo libre, puedes leer este excelente artículo sobre serialización . También habla de la interfaz Externalizable, de la que hablaremos en la próxima conferencia. Además, hay un capítulo sobre este tema en el libro "Head-First Java", preste atención :)
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION