Introducción a la serialización de Java

1. Introducción

La serialización es la conversión del estado de un objeto en un flujo de bytes; la deserialización hace lo contrario. Dicho de otra manera, la serialización es la conversión de un objeto Java en un flujo estático (secuencia) de bytes que luego se puede guardar en una base de datos o transferir a través de una red.

2. Serialización y deserialización

El proceso de serialización es independiente de la instancia, es decir, los objetos se pueden serializar en una plataforma y deserializar en otra. Las clases que son elegibles para la serialización necesitan implementar una interfaz de marcador especial Serializable.

Tanto ObjectInputStream como ObjectOutputStream son clases de alto nivel que amplían java.io.InputStream y java.io.OutputStream respectivamente. ObjectOutputStream puede escribir tipos primitivos y gráficos de objetos en un OutputStream como un flujo de bytes. Estos flujos se pueden leer posteriormente mediante ObjectInputStream .

El método más importante en ObjectOutputStream es:

public final void writeObject(Object o) throws IOException;

Que toma un objeto serializable y lo convierte en una secuencia (flujo) de bytes. Del mismo modo, el método más importante en ObjectInputStream es:

public final Object readObject() throws IOException, ClassNotFoundException;

Que puede leer un flujo de bytes y convertirlo nuevamente en un objeto Java. A continuación, se puede devolver al objeto original.

Ilustremos la serialización con una clase Person . Tenga en cuenta que los campos estáticos pertenecen a una clase (a diferencia de un objeto) y no están serializados . Además, tenga en cuenta que podemos usar la palabra clave transitoria para ignorar los campos de clase durante la serialización:

public class Person implements Serializable { private static final long serialVersionUID = 1L; static String country = "ITALY"; private int age; private String name; transient int height; // getters and setters }

La prueba a continuación muestra un ejemplo de cómo guardar un objeto de tipo Persona en un archivo local y luego leer este valor nuevamente en:

@Test public void whenSerializingAndDeserializing_ThenObjectIsTheSame() () throws IOException, ClassNotFoundException { Person person = new Person(); person.setAge(20); person.setName("Joe"); FileOutputStream fileOutputStream = new FileOutputStream("yourfile.txt"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(person); objectOutputStream.flush(); objectOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("yourfile.txt"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); Person p2 = (Person) objectInputStream.readObject(); objectInputStream.close(); assertTrue(p2.getAge() == p.getAge()); assertTrue(p2.getName().equals(p.getName())); }

Usamos ObjectOutputStream para guardar el estado de este objeto en un archivo usando FileOutputStream . El archivo "yourfile.txt" se crea en el directorio del proyecto. Luego, este archivo se carga usando FileInputStream. ObjectInputStream recoge este flujo y lo convierte en un nuevo objeto llamado p2 .

Finalmente, probamos el estado del objeto cargado y coincide con el estado del objeto original.

Tenga en cuenta que el objeto cargado debe convertirse explícitamente en un tipo Person .

3. Advertencias sobre la serialización de Java

Hay algunas advertencias que se refieren a la serialización en Java.

3.1. Herencia y Composición

Cuando una clase implementa la interfaz java.io.Serializable , todas sus subclases también son serializables. Por el contrario, cuando un objeto tiene una referencia a otro objeto, estos objetos deben implementar la interfaz serializable por separado, de lo contrario se lanzará una NotSerializableException :

public class Person implements Serializable { private int age; private String name; private Address country; // must be serializable too } 

Si uno de los campos en un objeto serializable consiste en una matriz de objetos, entonces todos estos objetos deben ser serializables también, o de lo contrario se lanzará una NotSerializableException .

3.2. UID de la versión de serie

La JVM asocia un número de versión ( largo ) con cada clase serializable. Se utiliza para verificar que los objetos guardados y cargados tienen los mismos atributos y, por lo tanto, son compatibles en la serialización.

Este número puede ser generado automáticamente por la mayoría de los IDE y se basa en el nombre de la clase, sus atributos y modificadores de acceso asociados. Cualquier cambio da como resultado un número diferente y puede causar una InvalidClassException .

Si una clase serializable no declara un serialVersionUID , la JVM generará uno automáticamente en tiempo de ejecución. Sin embargo, se recomienda encarecidamente que cada clase declare su serialVersionUID, ya que el generado depende del compilador y, por lo tanto, puede dar lugar a excepciones InvalidClassExceptions inesperadas .

3.3. Serialización personalizada en Java

Java especifica una forma predeterminada en la que se pueden serializar los objetos. Las clases de Java pueden anular este comportamiento predeterminado. La serialización personalizada puede ser particularmente útil cuando se intenta serializar un objeto que tiene algunos atributos no serializables. Esto se puede hacer proporcionando dos métodos dentro de la clase que queremos serializar:

private void writeObject(ObjectOutputStream out) throws IOException;

y

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;

Con estos métodos, podemos serializar esos atributos no serializables en otras formas que se pueden serializar:

public class Employee implements Serializable { private static final long serialVersionUID = 1L; private transient Address address; private Person person; // setters and getters private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); oos.writeObject(address.getHouseNumber()); } private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { ois.defaultReadObject(); Integer houseNumber = (Integer) ois.readObject(); Address a = new Address(); a.setHouseNumber(houseNumber); this.setAddress(a); } }
public class Address { private int houseNumber; // setters and getters }

La siguiente prueba unitaria prueba esta serialización personalizada:

@Test public void whenCustomSerializingAndDeserializing_ThenObjectIsTheSame() throws IOException, ClassNotFoundException { Person p = new Person(); p.setAge(20); p.setName("Joe"); Address a = new Address(); a.setHouseNumber(1); Employee e = new Employee(); e.setPerson(p); e.setAddress(a); FileOutputStream fileOutputStream = new FileOutputStream("yourfile2.txt"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(e); objectOutputStream.flush(); objectOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("yourfile2.txt"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); Employee e2 = (Employee) objectInputStream.readObject(); objectInputStream.close(); assertTrue( e2.getPerson().getAge() == e.getPerson().getAge()); assertTrue( e2.getAddress().getHouseNumber() == e.getAddress().getHouseNumber()); }

En este código, vemos cómo guardar algunos atributos que no se pueden serializar serializando Address con serialización personalizada. Tenga en cuenta que debemos marcar los atributos no serializables como transitorios para evitar la excepción NotSerializableException.

4. Conclusión

En este tutorial rápido, hemos revisado la serialización de Java, discutimos cosas importantes a tener en cuenta y hemos mostrado cómo hacer una serialización personalizada.

Como siempre, el código fuente utilizado en este tutorial está disponible en GitHub.