Serialización en Java. No tan simple



La serialización es un proceso que traduce un objeto en una secuencia de bytes, desde la cual se puede restaurar por completo. ¿Por qué se necesita esto? El hecho es que, durante la ejecución normal del programa, se conoce la vida útil máxima de cualquier objeto, desde el lanzamiento del programa hasta su finalización. La serialización le permite expandir este marco y "dar vida" al objeto de la misma manera entre los lanzamientos de programas.

Una ventaja adicional para todo es la preservación de la plataforma cruzada. No importa qué sistema operativo tenga, la serialización traduce el objeto en una secuencia de bytes, que puede restaurarse en cualquier sistema operativo. Si necesita transferir un objeto a través de la red, puede serializar el objeto, guardarlo en un archivo y transferirlo al destinatario a través de la red. Podrá recuperar el objeto recibido. La serialización también le permite llamar de forma remota métodos (Java RMI) que están en diferentes máquinas con, posiblemente, diferentes sistemas operativos, y trabajar con ellos como si estuvieran en la máquina del proceso de Java que realiza la llamada.

Implementar un mecanismo de serialización es bastante simple. Su clase necesita implementar la interfaz serializable . Esta interfaz es un identificador que no tiene métodos, pero le dice a jvm que los objetos de esta clase se pueden serializar. Dado que el mecanismo de serialización está conectado al sistema básico de entrada / salida y traduce el objeto en un flujo de bytes, para ejecutarlo debe crear un flujo de salida OutputStream , empaquetarlo en ObjectOutputStream y llamar al método writeObject (). Para restaurar un objeto, debe empacar un InputStream en un ObjectInputStream y llamar al método readObject ().

Durante el proceso de serialización, junto con el objeto serializable, se guarda su gráfico de objeto. Es decir todos los objetos relacionados con esto, los objetos de otras clases también se serializarán con él.

Considere un ejemplo de serialización de un objeto de clase Persona.

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

Conclusión

 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} 

En este ejemplo, la clase Home se crea para demostrar que al serializar un objeto Person , el gráfico de sus objetos se serializa con él. La clase Home también debe implementar la interfaz serializable , de lo contrario se producirá una excepción java.io.NotSerializableException . El ejemplo también describe la serialización utilizando la clase ByteArrayOutputStream .

Se puede sacar una conclusión interesante de los resultados de la ejecución del programa: al restaurar objetos que tenían una referencia al mismo objeto antes de la serialización, este objeto se restaurará solo una vez . Esto se puede ver en los mismos enlaces en los objetos después de la recuperación:

 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} 

Sin embargo, también se ve que cuando se graba con dos secuencias de salida (tenemos ObjectInputStream y ByteArrayOutputStream ), el objeto de inicio se volverá a crear, a pesar de que ya se creó antes en una de las secuencias. Vemos esto en diferentes direcciones de objetos domésticos recibidos en dos flujos. Resulta que si serializa con una secuencia de salida, luego restaura el objeto, entonces tenemos la garantía de restaurar la red completa de objetos sin duplicados innecesarios. Por supuesto, durante la ejecución del programa, el estado de los objetos puede cambiar, pero esto depende de la conciencia del programador.

El problema

El ejemplo también muestra que al restaurar un objeto, se puede producir una ClassNotFoundException . ¿Cuál es la razón de esto? El hecho es que podemos serializar fácilmente un objeto de la clase Person en un archivo, transferirlo a través de la red a nuestro amigo, quien puede restaurar el objeto mediante otra aplicación en la que la clase Person simplemente no existe.

Su serialización. Cómo hacer?

¿Qué sucede si desea gestionar la serialización usted mismo? Por ejemplo, su objeto almacena el nombre de usuario y la contraseña de los usuarios. Necesita serializarlo para una mayor transmisión a través de la red. Pasar una contraseña en este caso es extremadamente poco confiable. ¿Cómo resolver este problema? Hay dos formas Primero, use la palabra clave transitoria . Segundo, en lugar de darse cuenta del interés serializable , use su extensión: la interfaz externalizable . Considere los ejemplos del primer y segundo método para compararlos.

La primera forma: serialización usando transitorios

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

Conclusión

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

La segunda forma: serialización con la implementación de la interfaz externalizable

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

Conclusión

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

La primera diferencia entre las dos opciones que llama la atención es el tamaño del código. Al implementar la interfaz Externalizable , necesitamos anular dos métodos: writeExternal () y readExternal () . En el método writeExternal () , indicamos qué campos se serializarán y cómo, en readExternal () cómo leerlos. Cuando se usa la palabra transitoria, indicamos explícitamente qué campo o campos no necesitan ser serializados. También notamos que en el segundo método, creamos explícitamente un constructor predeterminado, además, uno público. ¿Por qué se hace esto? Intentemos ejecutar el código sin este constructor. Y mira la salida:

 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) 

Tenemos la excepción java.io.InvalidClassException . ¿Cuál es la razón de esto? Si sigue el seguimiento de la pila, puede descubrir que hay líneas en el constructor de la clase ObjectStreamClass :

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

Para la interfaz Externalizable , se llamará al método del constructor getExternalizableConstructor (), dentro del cual a través de Reflection intentaremos obtener el constructor predeterminado de la clase para la que estamos restaurando el objeto. Si no podemos encontrarlo, o no es público , entonces tenemos una excepción. Puede solucionar esta situación de la siguiente manera: no cree explícitamente ningún constructor en la clase y llene los campos utilizando setters y obtenga el valor con getters. Luego, al compilar la clase, se creará un constructor predeterminado, que estará disponible para getExternalizableConstructor () . Para Serializable, el método getSerializableConstructor () obtiene el constructor de la clase Object y busca la clase deseada, si no lo encuentra, obtenemos una excepción ClassNotFoundException . Resulta que la diferencia clave entre Serializable y Externalizable es que el primero no necesita un constructor para crear la recuperación de objetos. Simplemente se recuperará completamente de los bytes. Para el segundo, durante la restauración, primero se creará un objeto utilizando el constructor en el punto de declaración, y luego se escribirán en él los valores de sus campos de los bytes recibidos durante la serialización. Personalmente, prefiero el primer método, es mucho más simple. Además, incluso si aún necesitamos establecer el comportamiento de serialización, no podemos usar Externalizable , ni implementar Serializable agregando (sin anular) los métodos writeObject () y readObject () . Pero para que puedan "trabajar", su firma debe ser estrictamente 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); } } 

Conclusión

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

Dentro de nuestros métodos agregados, se llama defaultWriteObject () y defaultReadObject () . Son responsables de la serialización predeterminada, como si funcionase sin los métodos que agregamos.

De hecho, esto es solo la punta del iceberg, si continúa profundizando en el mecanismo de serialización, entonces con un alto grado de probabilidad, puede encontrar más matices, encontrando lo que decimos: "La serialización ... no es tan simple".

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


All Articles