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);
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 problemaEl 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".