JavaRush /Blog Java /Random-ES /Serialización tal como está. Parte 1
articles
Nivel 15

Serialización tal como está. Parte 1

Publicado en el grupo Random-ES
A primera vista, la serialización parece un proceso trivial. Realmente, ¿qué podría ser más sencillo? Declaró la clase para implementar la interfaz java.io.Serializable, y eso es todo. Puedes serializar la clase sin problemas. Serialización tal como está.  Parte 1 - 1Teóricamente, esto es cierto. En la práctica, hay muchas sutilezas. Están relacionados con el rendimiento, la deserialización y la seguridad de clase. Y con muchos más aspectos. Se discutirán tales sutilezas. Este artículo se puede dividir en las siguientes partes:
  • Sutilezas de los mecanismos.
  • ¿Por qué es necesario?Externalizable
  • Actuación
  • pero en la otra mano
  • Seguridad de datos
  • Serialización de objetosSingleton
Pasemos a la primera parte.

Sutilezas de los mecanismos.

Antes que nada, una pregunta rápida. ¿Cuántas formas hay de hacer que un objeto sea serializable? La práctica muestra que más del 90% de los desarrolladores responden a esta pregunta aproximadamente de la misma manera (hasta la redacción); solo hay una manera. Mientras tanto, hay dos de ellos. No todo el mundo recuerda el segundo y mucho menos dice algo inteligible sobre sus características. Entonces, ¿cuáles son estos métodos? Todos recuerdan el primero. Esta es la implementación ya mencionada java.io.Serializabley no requiere ningún esfuerzo. El segundo método también es la implementación de una interfaz, pero diferente: java.io.Externalizable. A diferencia de java.io.Serializable, contiene dos métodos que deben implementarse: writeExternal(ObjectOutput)y readExternal(ObjectInput). Estos métodos contienen la lógica de serialización/deserialización. Comentario.SerializableEn lo que sigue , a veces me referiré a la serialización con implementación como estándar y la implementación Externalizablecomo extendida. Otrocomentario. Deliberadamente no hablo ahora de opciones de control de serialización estándar como definir readObjecty writeObject, porque Creo que estos métodos son algo incorrectos. Estos métodos no están definidos en la interfaz Serializabley, de hecho, son accesorios para solucionar las limitaciones y hacer que la serialización estándar sea flexible. ExternalizableDesde el principio se incorporan métodos que proporcionan flexibilidad . Hagamos una pregunta más. ¿Cómo funciona realmente la serialización estándar usando java.io.Serializable? Y funciona a través de la API Reflection. Aquellos. la clase se analiza como un conjunto de campos, cada uno de los cuales se escribe en el flujo de salida. Creo que está claro que esta operación no es óptima en términos de rendimiento. Descubriremos cuánto exactamente más adelante. Existe otra diferencia importante entre los dos métodos de serialización mencionados. Es decir, en el mecanismo de deserialización. Cuando se usa, Serializablela deserialización ocurre así: se asigna memoria para un objeto, después de lo cual sus campos se llenan con valores de la secuencia. No se llama al constructor del objeto. Aquí debemos considerar esta situación por separado. Bien, nuestra clase es serializable. ¿Y su padre? ¡Completamente opcional! Además, si heredas una clase Object, el padre definitivamente NO es serializable. Y aunque Objectno sabemos nada sobre campos, es posible que existan en nuestras propias clases principales. que les pasara a ellos? No entrarán en el flujo de serialización. ¿Qué valores tomarán tras la deserialización? Veamos este ejemplo:
package ru.skipy.tests.io;

import java.io.*;

/**
 * ParentDeserializationTest
 *
 * @author Eugene Matyushkin aka Skipy
 * @since 05.08.2010
 */
public class ParentDeserializationTest {

    public static void main(String[] args){
        try {
            System.out.println("Creating...");
            Child c = new Child(1);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            c.field = 10;
            System.out.println("Serializing...");
            oos.writeObject(c);
            oos.flush();
            baos.flush();
            oos.close();
            baos.close();
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            System.out.println("Deserializing...");
            Child c1 = (Child)ois.readObject();
            System.out.println("c1.i="+c1.getI());
            System.out.println("c1.field="+c1.getField());
        } catch (IOException ex){
            ex.printStackTrace();
        } catch (ClassNotFoundException ex){
            ex.printStackTrace();
        }
    }

    public static class Parent {
        protected int field;
        protected Parent(){
            field = 5;
            System.out.println("Parent::Constructor");
        }
        public int getField() {
            return field;
        }
    }

    public static class Child extends Parent implements Serializable{
        protected int i;
        public Child(int i){
            this.i = i;
            System.out.println("Child::Constructor");
        }
        public int getI() {
            return i;
        }
    }
}
Es transparente: tenemos una clase principal no serializable y una clase secundaria serializable. Y esto es lo que pasa:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
Es decir, durante la deserialización, se llama al constructor sin parámetros de la clase principal NO serializable . Y si no existe tal constructor, se producirá un error durante la deserialización. El constructor del objeto hijo, el que estamos deserializando, no se llama, como se dijo anteriormente. Así es como se comportan los mecanismos estándar cuando se utilizan Serializable. Al usarlo, Externalizablela situación es diferente. Primero, se llama al constructor sin parámetros y luego se llama al método readExternal en el objeto creado, que en realidad lee todos sus datos. Por lo tanto, cualquier clase que implemente la interfaz externalizable debe tener un constructor público sin parámetros. Además, dado que también se considerará que todos los descendientes de dicha clase implementan la interfaz Externalizable, ¡también deben tener un constructor sin parámetros! Vayamos más lejos. Existe un modificador de campo como transient. Significa que este campo no debe serializarse. Sin embargo, como usted mismo comprende, esta instrucción solo afecta el mecanismo de serialización estándar. Cuando se usa, Externalizablenadie se molesta en serializar este campo ni en restarlo. Si un campo se declara transitorio, cuando el objeto se deserializa, adquiere el valor predeterminado. Otro punto bastante sutil. Con la serialización estándar, los campos que tienen el modificador staticno se serializan. En consecuencia, después de la deserialización, este campo no cambia su valor. Por supuesto, durante la implementación, Externalizablenadie se molesta en serializar y deserializar este campo, pero recomiendo encarecidamente no hacerlo, porque esto puede provocar errores sutiles. Los campos con un modificador finalse serializan como campos normales. Con una excepción: no se pueden deserializar cuando se usa Externalizable. Porque final-поляdeben inicializarse en el constructor, y luego será imposible cambiar el valor de este campo en readExternal. En consecuencia, si necesita serializar un objeto que tiene finalun campo -, sólo tendrá que utilizar la serialización estándar. Otro punto que mucha gente desconoce. La serialización estándar tiene en cuenta el orden en que se declaran los campos en una clase. En cualquier caso, este era el caso en versiones anteriores, en la versión 1.6 de JVM de la implementación de Oracle, el orden ya no es importante, el tipo y el nombre del campo son importantes. Es muy probable que la composición de los métodos afecte al mecanismo estándar, a pesar de que los campos en general pueden seguir siendo los mismos. Para evitar esto, existe el siguiente mecanismo. A cada clase que implementa la interfaz Serializable, se agrega un campo más en la etapa de compilación:private static final long serialVersionUID. Este campo contiene el identificador de versión único de la clase serializada. Se calcula en función del contenido de la clase: campos, su orden de declaración, métodos, su orden de declaración. En consecuencia, con cualquier cambio en la clase, este campo cambiará su valor. Este campo se escribe en la secuencia cuando se serializa la clase. Por cierto, este es quizás el único caso que conozco cuando staticse serializa un campo. Durante la deserialización, el valor de este campo se compara con el de la clase en la máquina virtual. Si los valores no coinciden, se lanza una excepción como esta:
java.io.InvalidClassException: test.ser2.ChildExt;
    local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
                                   local class serialVersionUID = 1465687698753363969
Sin embargo, existe una manera de, si no eludir, engañar este control. Esto puede resultar útil si el conjunto de campos de clase y su orden ya están definidos, pero los métodos de clase pueden cambiar. En este caso, la serialización no está en riesgo, pero el mecanismo estándar no permitirá que los datos se deserialicen utilizando el código de bytes de la clase modificada. Pero, como dije, se le puede engañar. Es decir, defina manualmente el campo en la clase private static final long serialVersionUID. En principio, el valor de este campo puede ser absolutamente cualquier cosa. Algunas personas prefieren establecerlo igual a la fecha en que se modificó el código. Algunos incluso usan 1L. Para obtener el valor estándar (el que se calcula internamente), puedes utilizar la utilidad serialver incluida en el SDK. Una vez definido de esta manera, el valor del campo será fijo, por lo que siempre se permitirá la deserialización. Además, en la versión 5.0, apareció aproximadamente lo siguiente en la documentación: se recomienda encarecidamente que todas las clases serializables declaren este campo explícitamente, porque el cálculo predeterminado es muy sensible a los detalles de la estructura de clases, que pueden variar según la implementación del compilador. y así provocar InvalidClassExceptionconsecuencias inesperadas... deserialización. Es mejor declarar este campo como private, porque se refiere únicamente a la clase en la que se declara. Aunque el modificador no está especificado en la especificación. Consideremos ahora este aspecto. Digamos que tenemos esta estructura de clases:
public class A{
    public int iPublic;
    protected int iProtected;
    int iPackage;
    private int iPrivate;
}

public class B extends A implements Serializable{}
En otras palabras, tenemos una clase heredada de un padre no serializable. ¿Es posible serializar esta clase y qué se necesita para ello? ¿Qué pasará con las variables de la clase padre? La respuesta es esta. Sí, Bpuedes serializar una instancia de una clase. ¿Qué se necesita para esto? Pero la clase necesita Atener un constructor sin parámetros publico protected. Luego, durante la deserialización, todas las variables de clase Ase inicializarán utilizando este constructor. Las variables de clase Bse inicializarán con los valores del flujo de datos serializados. Teóricamente, es posible definir en una clase Blos métodos de los que hablé al principio - readObjecty writeObject- al principio de los cuales realizar (des)serialización de variables de clase Ba través de in.defaultReadObject/out.defaultWriteObject, y luego (des)serialización de variables disponibles de la clase A(en nuestro caso son iPublic, iProtectedy iPackage, si Bestá en el mismo paquete que A). Sin embargo, en mi opinión, es mejor utilizar la serialización extendida para esto. El siguiente punto que me gustaría tocar es la serialización de múltiples objetos. Digamos que tenemos la siguiente estructura de clases:
public class A implements Serializable{
    private C c;
    private B b;
    public void setC(C c) {this.c = c;}
    public void setB(B b) {this.b = b;}
    public C getC() {return c;}
    public B getB() {return b;}
}
public class B implements Serializable{
    private C c;
    public void setC(C c) {this.c = c;}
    public C getC() {return c;}
}
public class C implements Serializable{
    private A a;
    private B b;
    public void setA(A a) {this.a = a;}
    public void setB(B b) {this.b = b;}
    public B getB() {return b;}
    public A getA() {return a;}
}
Serialización tal como está.  Parte 1 - 2¿ Qué sucede si serializas una instancia de la clase A? Arrastrará una instancia de la clase B, que a su vez arrastrará una instancia Cque tiene referencia a la instancia A, la misma con la que empezó todo. ¿Círculo vicioso y recursividad infinita? Afortunadamente, no. Veamos el siguiente código de prueba:
// initiaizing
A a = new A();
B b = new B();
C c = new C();
// setting references
a.setB(b);
a.setC(c);
b.setC(c);
c.setA(a);
c.setB(b);
// serializing
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
oos.writeObject(b);
oos.writeObject(c);
oos.flush();
oos.close();
// deserializing
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
A a1 = (A)ois.readObject();
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
// testing
System.out.println("a==a1: "+(a==a1));
System.out.println("b==b1: "+(b==b1));
System.out.println("c==c1: "+(c==c1));
System.out.println("a1.getB()==b1: "+(a1.getB()==b1));
System.out.println("a1.getC()==c1: "+(a1.getC()==c1));
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1.getA()==a1: "+(c1.getA()==a1));
System.out.println("c1.getB()==b1: "+(c1.getB()==b1));
¿Que estamos haciendo? Creamos una instancia de las clases Ay Bles Cdamos enlaces entre sí y luego serializamos cada una de ellas. Luego los deserializamos nuevamente y ejecutamos una serie de comprobaciones. ¿Qué sucederá como resultado?
a==a1: false
b==b1: false
c==c1: false
a1.getB()==b1: true
a1.getC()==c1: true
b1.getC()==c1: true
c1.getA()==a1: true
c1.getB()==b1: true
Entonces, ¿qué puedes aprender de esta prueba? Primero. Las referencias a objetos después de la deserialización son diferentes de las referencias anteriores. En otras palabras, durante la serialización/deserialización se copió el objeto. Este método se utiliza a veces para clonar objetos. La segunda conclusión es más significativa. Al serializar/deserializar varios objetos que tienen referencias cruzadas, esas referencias siguen siendo válidas después de la deserialización. En otras palabras, si antes de la serialización apuntaron a un objeto, después de la deserialización también apuntarán a un objeto. Otra pequeña prueba para confirmar esto:
B b = new B();
C c = new C();
b.setC(c);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
oos.writeObject(c);
oos.writeObject(c);
oos.writeObject(c);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
C c2 = (C)ois.readObject();
C c3 = (C)ois.readObject();
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1==c2: "+(c1==c2));
System.out.println("c1==c3: "+(c1==c3));
Un objeto de clase Btiene una referencia a un objeto de clase C. Cuando se serializa, bse serializa junto con una instancia de la clase С, después de lo cual la misma instancia de c se serializa tres veces. ¿Qué sucede después de la deserialización?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
Como puede ver, los cuatro objetos deserializados en realidad representan un objeto: las referencias a él son iguales. Exactamente como era antes de la serialización. Otro punto interesante: ¿qué pasará si implementamos simultáneamente Externalizabley Serializable? Como en esa pregunta - elefante versus ballena - ¿quién derrotará a quién? Superará Externalizable. El mecanismo de serialización primero verifica su presencia, y solo luego, Serializablepor lo que si la clase B, que implementa Serializable, hereda de la clase A, que implementa Externalizable, los campos de la clase B no serán serializados. El último punto es la herencia. Al heredar de una clase que implementa Serializable, no es necesario realizar acciones adicionales. La serialización también se extenderá a la clase secundaria. Al heredar de una clase que implementa Externalizable, debe anular los métodos readExternal y writeExternal de la clase principal. De lo contrario, los campos de la clase secundaria no se serializarán. En este caso, deberá recordar llamar a los métodos principales; de lo contrario, los campos principales no se serializarán. * * * Probablemente hayamos terminado con los detalles. Sin embargo, hay un tema que no hemos tocado, de carácter global. Es decir -

¿Por qué necesitas externalizable?

¿Por qué necesitamos una serialización avanzada? La respuesta es simple. En primer lugar, da mucha más flexibilidad. En segundo lugar, a menudo puede proporcionar ganancias significativas en términos del volumen de datos serializados. En tercer lugar, existe un aspecto como el rendimiento, del que hablaremos a continuación . Todo parece estar claro con la flexibilidad. De hecho, podemos controlar los procesos de serialización y deserialización como queramos, lo que nos hace independientes de cualquier cambio en la clase (como dije anteriormente, los cambios en la clase pueden afectar en gran medida la deserialización). Por lo tanto, quiero decir algunas palabras sobre el aumento de volumen. Digamos que tenemos la siguiente clase:
public class DateAndTime{

  private short year;
  private byte month;
  private byte day;
  private byte hours;
  private byte minutes;
  private byte seconds;

}
El resto no tiene importancia. Los campos podrían ser de tipo int, pero esto sólo mejoraría el efecto del ejemplo. Aunque en realidad es posible que los campos estén tipificados intpor cuestiones de rendimiento. En cualquier caso, el punto está claro. La clase representa una fecha y hora. Nos interesa principalmente desde el punto de vista de la serialización. Quizás lo más fácil sería almacenar una marca de tiempo simple. Es de tipo largo, es decir cuando se serializa, tomaría 8 bytes. Además, este enfoque requiere métodos para convertir componentes a un valor y viceversa, es decir – pérdida de productividad. La ventaja de este enfoque es una fecha completamente loca que cabe en 64 bits. Se trata de un enorme margen de seguridad que, en la mayoría de los casos, en la realidad no es necesario. La clase dada anteriormente tomará 2 + 5*1 = 7 bytes. Más gastos generales para la clase y 6 campos. ¿Hay alguna manera de comprimir estos datos? Con seguridad. Los segundos y minutos están en el rango 0-59, es decir para representarlos, son suficientes 6 bits en lugar de 8. Horas – 0-23 (5 bits), días – 0-30 (5 bits), meses – 0-11 (4 bits). Total, todo sin tener en cuenta el año: 26 bits. Todavía quedan 6 bits del tamaño de int. En teoría, en algunos casos esto puede ser suficiente para un año. De lo contrario, agregar otro byte aumenta el tamaño del campo de datos a 14 bits, lo que da un rango de 0-16383. Esto es más que suficiente en aplicaciones reales. En total, hemos reducido el tamaño de los datos necesarios para almacenar la información necesaria a 5 bytes. Si no hasta 4. La desventaja es la misma que en el caso anterior: si almacena el dátil empaquetado, entonces se necesitarán métodos de conversión. Pero quiero hacerlo de esta manera: almacenarlo en campos separados y serializarlo en forma empaquetada. Aquí es donde tiene sentido utilizar Externalizable:
// data is packed into 5 bytes:
//  3         2         1
// 10987654321098765432109876543210
// hhhhhmmmmmmssssssdddddMMMMyyyyyy yyyyyyyy
public void writeExternal(ObjectOutput out){
    int packed = 0;
    packed += ((int)hours) << 27;
    packed += ((int)minutes) << 21;
    packed += ((int)seconds) << 15;
    packed += ((int)day) << 10;
    packed += ((int)month) << 6;
    packed += (((int)year) >> 8) & 0x3F;
    out.writeInt(packed);
    out.writeByte((byte)year);
}

public void readExternal(ObjectInput in){
    int packed = in.readInt();
    year = in.readByte() & 0xFF;
    year += (packed & 0x3F) << 8;
    month = (packed >> 6) & 0x0F;
    day = (packed >> 10) & 0x1F;
    seconds = (packed >> 15) & 0x3F;
    minutes = (packed >> 21) & 0x3F;
    hours = (packed >> 27);
}
En realidad, eso es todo. Después de la serialización, obtenemos gastos generales por clase, dos campos (en lugar de 6) y 5 bytes de datos. Lo cual ya es mucho mejor. El resto del embalaje se puede dejar en manos de bibliotecas especializadas. El ejemplo dado es muy sencillo. Su objetivo principal es mostrar cómo se puede utilizar la serialización avanzada. Aunque, en mi opinión, la posible ganancia en el volumen de datos serializados está lejos de ser la principal ventaja. La principal ventaja, además de la flexibilidad... (pase sin problemas a la siguiente sección...) Enlace a la fuente: Serialización tal como está
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION