CodeGym /Cursos /JAVA 25 SELF /Serialización de colecciones genéricas: particularidades

Serialización de colecciones genéricas: particularidades

JAVA 25 SELF
Nivel 45, Lección 1
Disponible

1. Serialización y deserialización de colecciones genéricas

Los genéricos (tipos parametrizados) en Java no son magia, sino más bien una ilusión mantenida por el compilador. En la fase de compilación, la información sobre los tipos de los parámetros genéricos se borra (esto se llama type erasure, o «borrado de tipos»). Es decir, en tiempo de ejecución (runtime) la colección List<String> no se diferencia de List<Object> o List<Integer>. Todas ellas son simplemente List, y la JVM no sabe qué tipos hay dentro.

Veamos un ejemplo:

List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

System.out.println(stringList.getClass() == intList.getClass()); // true!

Aquí todo está montado con astucia. Creamos dos listas: una para cadenas y otra para números. En tiempo de compilación, Java vigila estrictamente que no metas en List<String> algo que no sea una cadena, y en List<Integer> algo que no sea un número. Pero en cuanto el programa arranca, las diferencias desaparecen. Para la JVM ambos objetos son simplemente ArrayList, y ya no puede comprobar qué elementos se supone que deben almacenarse allí. Por eso la comparación de clases de ambas listas (stringList.getClass() == intList.getClass()) devuelve true.

De aquí una conclusión importante: los genéricos en Java existen ante todo por comodidad y seguridad en la fase de compilación. Pero en tiempo de ejecución esas «etiquetas» se pierden. Por lo tanto, si serializas una colección, al archivo solo irán los datos en sí, no la información sobre los tipos genéricos. Es decir, se guardará la lista de valores, pero por el archivo no se puede saber si era exactamente un List<String>, o un List<Object> o un List<Integer>.

Otro ejemplo: serialización y deserialización de List<String>

import java.io.*;
import java.util.*;

public class GenericSerializationDemo {
    public static void main(String[] args) throws Exception {
        List<String> fruits = new ArrayList<>();
        fruits.add("Manzana");
        fruits.add("Plátano");
        fruits.add("Naranja");

        // Serialización
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fruits.ser"))) {
            oos.writeObject(fruits);
        }

        // Deserialización
        List<String> loadedFruits;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fruits.ser"))) {
            loadedFruits = (List<String>) ois.readObject();
        }

        System.out.println(loadedFruits); // [Manzana, Plátano, Naranja]
    }
}

Fíjate en la línea:

loadedFruits = (List<String>) ois.readObject();

Aquí convertimos explícitamente el resultado al tipo List<String>, aunque en realidad en tiempo de ejecución es simplemente un ArrayList. El compilador no puede comprobar que realmente sea una lista de cadenas, y si de repente hubiese algo que no fueran cadenas, obtendremos ClassCastException ya durante la ejecución del programa.

2. Problemas al deserializar colecciones genéricas

Pérdida de información sobre el tipo de los elementos

Como la información de los parámetros genéricos se borra, después de la deserialización Java no puede garantizar que en la colección haya exactamente los objetos que esperas. Lo único que obtienes es una colección «raw type» (raw type), y el compilador no se queja, pero el problema puede aflorar en tiempo de ejecución.

Demostración del problema

List rawList = new ArrayList();
rawList.add("Gato");
rawList.add(42); // Integer!
// Deserialización
List<String> loadedCats = (List<String>) ois.readObject();
String cat = loadedCats.get(1); // ClassCastException!

Advertencia de conversión no comprobada (unchecked cast)

El compilador te avisará de un posible problema:

Note: GenericSerializationDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

Esta advertencia indica que estás realizando una conversión sin comprobación, y en la colección podrían existir objetos de un tipo inesperado.

3. Particularidades de la serialización de colecciones genéricas

No hay información de parámetros genéricos en el archivo

Cuando serializas List<String> y List<Integer>, en el archivo no habrá ninguna información sobre si eran cadenas o números. El contenido de la colección se serializa tal cual: los objetos en orden.

Si abres el archivo serializado en un editor de texto, no verás ninguna mención de <String> o <Integer>. Todo eso existe solo a nivel de código fuente y del compilador.

Ejemplo: serialización de distintas colecciones

List<Integer> numbers = Arrays.asList(1, 2, 3);
List<String> words = Arrays.asList("uno", "dos", "tres");

// Serialización
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"))) {
    oos.writeObject(numbers);
    oos.writeObject(words);
}

En el archivo test.ser hay simplemente dos objetos de tipo ArrayList, sin ninguna información sobre los parámetros genéricos.

Problema al deserializar colecciones «raw»

Si serializaste una lista sin parámetro genérico (raw type) y la deserializas como List<String>, el compilador no podrá verificar la corrección de tipos y podrías tener errores en tiempo de ejecución.

4. Buenas prácticas en la serialización de colecciones genéricas

Documenta los tipos de elementos esperados.
Si tu API serializa una colección, indica siempre qué tipo de elementos se espera. Por ejemplo: «Este método devuelve un List<User> serializado».

Comprueba los tipos de los elementos tras la deserialización.
Después de deserializar una colección, conviene verificar que todos los elementos tengan el tipo esperado (especialmente si la fuente de datos no está bajo tu control).

for (Object obj : loadedList) {
    if (!(obj instanceof String)) {
        throw new IllegalStateException("Se esperaba una cadena, pero se encontró: " + obj.getClass());
    }
}

Usa colecciones inmutables.
Si serializas una colección de solo lectura, utiliza colecciones inmutables — List.copyOf, Collections.unmodifiableList. Esto ayudará a evitar cambios accidentales en los datos después de la deserialización.

No mezcles tipos en una misma colección.
Evita serializar colecciones con elementos de distintos tipos (por ejemplo, un List<Object> con clases variadas dentro). Esto complica la deserialización y puede provocar errores.

Usa con cuidado la supresión de advertencias.
Si estás seguro de que deserializas una colección con el tipo de elementos correcto, puedes suprimir la advertencia del compilador con la anotación @SuppressWarnings("unchecked"):

@SuppressWarnings("unchecked")
List<String> loaded = (List<String>) ois.readObject();

Pero úsalo con criterio: es fácil ocultar un problema hasta producción.

5. Ejemplo: serialización y deserialización de una colección con una clase propia

Supongamos que tenemos una clase User:

import java.io.Serializable;

public class User implements Serializable {
    private String name;
    private int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String toString() {
        return name + " (" + age + ")";
    }
}

Serializamos una lista de usuarios:

List<User> users = Arrays.asList(
    new User("Alicia", 30),
    new User("Bob", 25)
);

// Serialización
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users.ser"))) {
    oos.writeObject(users);
}

// Deserialización
List<User> loadedUsers;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users.ser"))) {
    loadedUsers = (List<User>) ois.readObject();
}

System.out.println(loadedUsers); // [Alicia (30), Bob (25)]

¡Todo funciona! Pero si alguien introduce en el archivo serializado un objeto de otra clase, puedes obtener un ClassCastException al intentar leer los elementos como User.

6. Serialización de colecciones genéricas anidadas

Las colecciones pueden estar anidadas, por ejemplo: List<List<String>>, Map<String, List<User>>, etc. Java serializa estas estructuras de forma recursiva, pero las reglas siguen siendo las mismas:

  • Todas las colecciones anidadas y sus elementos deben ser serializables.
  • La información sobre los parámetros genéricos sigue borrándose.

Ejemplo: serialización de una lista de listas

List<List<String>> matrix = new ArrayList<>();
matrix.add(Arrays.asList("a", "b"));
matrix.add(Arrays.asList("c", "d"));

// Serialización
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("matrix.ser"))) {
    oos.writeObject(matrix);
}

// Deserialización
List<List<String>> loadedMatrix;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("matrix.ser"))) {
    loadedMatrix = (List<List<String>>) ois.readObject();
}

System.out.println(loadedMatrix); // [[a, b], [c, d]]

7. Matices útiles

Serialización de colecciones genéricas con distintas implementaciones

A veces serializas una implementación de colección y deserializas como otra. Por ejemplo, serializaste un ArrayList, pero deserializas como LinkedList. Esto provocará un error de conversión de tipos:

List<String> list = new ArrayList<>();
// ...
List<String> loaded = (LinkedList<String>) ois.readObject(); // ClassCastException!

Consejo: Deserializa siempre al mismo tipo que serializaste, o usa la interfaz (List) si no te importa la implementación concreta.

Uso de bibliotecas (por ejemplo, Gson, Jackson)

Las bibliotecas para JSON (por ejemplo, Gson, Jackson) pueden serializar/deserializar colecciones con genéricos, pero requieren indicar explícitamente el tipo al deserializar debido al borrado de tipos. Ejemplo para Gson:

Type type = new com.google.gson.reflect.TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);

8. Genéricos y serialización en Map y Set

Todas las reglas anteriores también se aplican a otras colecciones con genéricos:

  • Al serializar una Map<String, Integer>, no se guarda la información sobre los tipos de las claves y los valores.
  • Al deserializar, hay que convertir al tipo necesario y prestar atención al contenido.

Ejemplo: serialización de Map

Map<String, Integer> scores = new HashMap<>();
scores.put("Bill", 90);
scores.put("Pete", 85);

// Serialización
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("scores.ser"))) {
    oos.writeObject(scores);
}

// Deserialización
Map<String, Integer> loadedScores;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("scores.ser"))) {
    loadedScores = (Map<String, Integer>) ois.readObject();
}

System.out.println(loadedScores); // {Bill=90, Pete=85}

9. Errores típicos en la serialización de colecciones genéricas

Error n.º 1: ClassCastException al deserializar. Si deserializas una colección como List<String>, pero contiene un objeto de otro tipo, obtendrás ClassCastException en tiempo de ejecución. ¡Comprueba siempre el contenido de la colección!

Error n.º 2: NotSerializableException debido a un elemento no serializable. Si al menos un elemento de la colección no implementa Serializable, la serialización fallará con NotSerializableException. Comprueba la serializabilidad de todas las clases que puedan aparecer en la colección.

Error n.º 3: Pérdida de información de los parámetros genéricos. Tras la deserialización, no te fíes de los parámetros genéricos: no existen en tiempo de ejecución. Usa comprobaciones explícitas de tipos si dudas de la corrección de los datos.

Error n.º 4: No coinciden las implementaciones de las colecciones. Serializaste un ArrayList y deserializas como LinkedList: obtendrás un error de conversión. Intenta deserializar al mismo tipo que fue serializado.

Error n.º 5: Incompatibilidad de versiones de clases. Si la estructura de la clase de un elemento de la colección cambió después de la serialización (por ejemplo, añadiste un campo), pueden producirse errores en la deserialización. Usa serialVersionUID para el control de versiones.

1
Tarea
JAVA 25 SELF, nivel 45, lección 1
Bloqueada
Caja misteriosa: comprobación del contenido tras la extracción
Caja misteriosa: comprobación del contenido tras la extracción
1
Tarea
JAVA 25 SELF, nivel 45, lección 1
Bloqueada
Asistente personal de compras: guardar el plan semanal
Asistente personal de compras: guardar el plan semanal
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION