¿Qué es serialVersionUID en Java?

En este tutorial, explicaremos qué es serialVersionUID en Java y ejecutaremos un ejemplo de serialización y deserialización usándolo.

Introducción

En este artículo, discutiremos un concepto relacionado con la serialización y deserialización en Java. Aunque a veces se considera como "parte de la magia negra de la API de serialización de Java", en este artículo veremos que serialVersionUID es de hecho bastante directo y simple.

Primero, pasaremos por alto la serialización y la deserialización para recordar algunas ideas importantes que necesitaremos más adelante. Luego, profundizaremos en serialVersionUID y mostraremos qué es y cómo funciona.

Finalmente, concluiremos mostrando un ejemplo que debería unir todo.

Serialización y deserialización

La serialización es el proceso de almacenar el estado de un objeto para que pueda persistir en una base de datos, transferirse a través de la red, escribirse en un archivo, etc. Cómo exactamente funciona la serialización está más allá del alcance de este artículo, pero en general: funciona convirtiendo el objeto en un flujo de bytes que luego se puede usar como cualquier otro flujo de información, p. transferido a través de un socket de red.

La deserialización es el proceso opuesto a la serialización. Toma la representación de flujo de bytes de un objeto (por ejemplo, de un archivo o un socket) y la vuelve a convertir en un objeto Java que vive dentro de la JVM.

Antes de que se pueda realizar la serialización o la deserialización en un objeto, es necesario que este objeto (es decir, su clase) implemente la interfaz Serializable. La interfaz Serializable se usa para "marcar" clases que pueden ser (des) serializadas.

Sin una clase que implemente esta interfaz, no es posible serializar o deserializar objetos de esa clase. En palabras de Javadoc serializable:

"La serialización de una clase está habilitada por la clase que implementa la interfaz java.io.Serializable*.

¿Qué es serialVersionUID? {#cuál es el ID de versión de serie}

Para que la serialización y la deserialización funcionen correctamente, cada clase serializable debe tener un número de versión asociado: serialVersionUID. El propósito de este valor es asegurarse de que las clases utilizadas tanto por el emisor (el que serializa) como por el receptor (el que deserializa) del objeto serializado son compatibles entre sí.

Si pensamos en esto, tiene mucho sentido. Debería haber algún mecanismo para determinar si el objeto que se envió coincide con el objeto que se recibió. De lo contrario, podría suceder, por ejemplo, que se haya realizado un cambio en la clase de un objeto antes de su serialización del que el receptor no sea consciente.

Al leer el objeto (es decir, deserialización), el lector podría cargar el objeto "nuevo" en la representación "antigua". En el mejor de los casos, esto podría tener consecuencias molestas y, en el peor de los casos, un completo desorden de la lógica empresarial.

Esa es precisamente la razón por la cual serialVersionUID existe y se usa normalmente con todos los objetos serializables. Se utiliza para verificar que ambas "versiones" de un objeto (del lado del emisor y del receptor) son compatibles, es decir, idénticas.

En caso de que sea necesario realizar una actualización en la clase, esto puede indicarse incrementando el valor de serialVersionUID. La versión serializada tendrá así un UID actualizado que se almacenará junto con el objeto y se entregará al lector.

Si el lector no tiene la versión más reciente de la clase, se generará una InvalidClassException.

¿Cómo generar serialVersionUID?

Según la documentación, cada campo serialVersionUID debe ser ’estático’, ‘final’ y de tipo ’largo’. El modificador de acceso puede ser arbitrario, pero se recomienda encarecidamente que todas las declaraciones utilicen el modificador private.

En ese caso, el modificador solo se aplicará a la clase actual y no a sus subclases, que es el comportamiento esperado; no queremos que una clase sea influenciada por otra cosa que no sea ella misma. Con todo lo dicho, así es como se vería un ‘serialVersionUID’ correctamente construido:

1
private static final long serialVersionUID = 42L;

Anteriormente mencionamos que todas las clases serializables deben implementar la interfaz Serializable.

Esta interfaz sugiere que todas las clases serializables pueden declarar un serialVersionUID, pero no están obligadas a hacerlo. En caso de que una clase no tenga un valor serialVersionUID declarado explícitamente, el tiempo de ejecución de serialización generará uno.

Sin embargo, es fuertemente recomendado que todas las clases serializables declaren explícitamente un valor serialVersionUID.

Esto se debe a que el cálculo predeterminado de serialVersionUID es complejo y, por lo tanto, sensible a diferencias muy leves en los entornos. Si se usan dos compiladores diferentes en el proceso de serialización-deserialización, se puede lanzar una InvalidClassException durante la deserialización porque las clases aparentemente no coincidirán aunque tengan el mismo contenido, palabra por palabra.

Finalmente, si hay algún campo “transitorio” o “estático” presente en la clase, se ignorará durante el proceso de serialización y será “nulo” después de la deserialización.

ejemplo serialVersionUID

Definamos una clase que usaremos para serialización y deserialización. Por supuesto, implementará la interfaz Serializable y comenzaremos con serialVersionUID siendo 1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Spaceship implements Serializable {

    private static final long serialVersionUID = 1L;

    private Pilot pilot;
    private Engine engine;
    private Hyperdrive hyperdrive;

    public void fly() {
        System.out.println("We're about to fly high among the stars!");
    }

    // Constructor, Getters, Setters
}

A continuación, implementaremos un método serializeObject() que será responsable de serializar el objeto y escribirlo en un archivo .ser:

1
2
3
4
5
6
7
8
public void serializeObject(Spaceship spaceship) {
    ObjectOutputStream out = new ObjectOutputStream(
        new FileOutputStream("./spaceship.ser")
    );

    out.writeObject(spaceship);
    out.close();
}

Nuestro método serializa el objeto spaceship en un archivo .ser a través de FileOutputStream. Este archivo ahora contiene el contenido serializado de nuestro objeto.

Ahora, implementemos un método deserializeObject(), que toma ese archivo .ser y construye un objeto a partir de él:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void deserializeObject(String filepath) {
    Spaceship ship;

    ObjectInputStream in = new ObjectInputStream(
        new FileInputStream(filepath)
    );
        
    ship = (Spaceship) in.readObject();
    in.close();

    ship.fly();
}

Llamemos a estos dos y observemos el resultado:

1
2
3
4
5
6
7
public class Main {
    public static void main(String[] args) {
        Spaceship spaceship = new Spaceship();
        serializeObject(spaceship);
        deserializeObject("./spaceship.ser");
    }
}

Esto dará como resultado:

1
We're about to fly high among the stars!

Nuestro método deserializeObject() cargó el archivo serializado en la JVM y lo convirtió con éxito en un objeto Spaceship.

Para demostrar el problema mencionado anteriormente con respecto al control de versiones, cambiemos el valor de serialVersionUID de 1L a 2L en nuestra clase Spaceship.

Luego, modifiquemos nuestro método main() para leer el archivo nuevamente, sin escribirlo con el serialVersionUID modificado:

1
2
3
4
5
public class Main {
    public static void main(String[] args) {
        deserializeObject("./spaceship.ser");
    }
}

Por supuesto, esto resultará en:

1
Exception in thread "main" java.io.InvalidClassException ...

Como era de esperar, el motivo de la excepción radica en serialVersionUID.

Debido a que no hemos escrito los nuevos datos después de actualizar el valor serialVersionUID a 2L, el objeto serializado aún contiene 1L como su serialVersionUID.

Sin embargo, el método deserializeObject() esperaba que este valor fuera 2L porque ese es el nuevo valor real dentro de la instancia Spaceship. Debido a esta inconsistencia entre el estado almacenado y el restaurado del objeto Spaceship, la excepción fue lanzada apropiadamente.

Conclusión

La serialización y la deserialización son técnicas poderosas y comunes que se utilizan para almacenar o transmitir objetos y estructuras de datos. A veces es fácil pasar por alto ciertos detalles importantes, como el serialVersionUID, especialmente dado que los IDE suelen generarlo automáticamente.

Con suerte, ahora debería estar un poco más claro cuál es su propósito y cómo usarlo correctamente en los proyectos venideros.

Licensed under CC BY-NC-SA 4.0