Serialización en Java

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 otro modo, la serialización es la conversión de un objeto Java en un flujo estático (secuencia) de bytes, que luego podemos guardar en una base de datos o transferir a través de una red.

Serialización y deserialización

El proceso de serialización es independiente de la instancia; por ejemplo, podemos serializar objetos en una plataforma y deserializarlos en otra. Las clases que son aptas para la serialización deben implementar una interfaz de marcador especial, Serializable.

Tanto ObjectInputStream como ObjectOutputStream son clases de alto nivel que extienden 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. Luego podemos leer estos flujos usando ObjectInputStream.

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

public final void writeObject(Object o) throws IOException;

Este método toma un objeto serializable y lo convierte en una secuencia (flujo) de bytes. De manera similar, el método más importante en ObjectInputStream es:

Este método puede leer un flujo de bytes y convertirlo nuevamente en un objeto Java. Luego, se puede convertir nuevamente en el objeto original.

Ilustremos la serialización con una clase Person. Tenga en cuenta que los campos estáticos pertenecen a una clase (en lugar de a un objeto) y no se serializan. Además, tenga en cuenta que podemos usar la palabra clave transient 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;
}

Veamos un ejemplo con el objecto Person:

public void serializacion() 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(); 
}

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 toma este flujo y lo convierte en un nuevo objeto llamado p2.

Por último, probaremos el estado del objeto cargado y nos aseguraremos de que coincida con el estado del objeto original.

Advertencias sobre la serialización en Java

Existen algunas advertencias relacionadas con 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 o, de lo contrario, se lanzará una excepción NotSerializableException:

public class Person implements Serializable {
    private int age;
    private String name;
    private Address country; // tiene que ser serializable también
}

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

Serial Version UID

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

La mayoría de los IDE pueden generar este número automáticamente y se basa en el nombre de la clase, los atributos y los 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, es muy recomendable que cada clase declare su serialVersionUID, ya que el generado depende del compilador y, por lo tanto, puede generar InvalidClassExceptions inesperadas.

Serialización personalizada en Java

Java especifica una forma predeterminada de serializar objetos, pero las clases 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. Podemos hacerlo 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 los atributos no serializables en otras formas que podemos serializar:

public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    private transient Address address;
    private Person person;

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

Podemos ejecutar el siguiente código para probar esta serialización personalizada:

public void customSerialization() 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();
}