1. Serializzazione e deserializzazione di collezioni generics
I generics (generalizzazioni) in Java non sono magia, ma piuttosto un’illusione mantenuta dal compilatore. In fase di compilazione l’informazione sui tipi dei parametri generics viene cancellata (si chiama type erasure, ovvero «cancellazione dei tipi»). Cioè a tempo di esecuzione (runtime) la collezione List<String> non si distingue da List<Object> o List<Integer>. Sono tutte semplicemente List, e la JVM non sa quali tipi esatti vi siano contenuti.
Consideriamo un esempio:
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.getClass() == intList.getClass()); // true!
Qui tutto è organizzato in modo sottile. Creiamo due liste — una per le stringhe, l’altra per i numeri. A livello di compilatore Java controlla rigorosamente che non mettiate in List<String> nulla che non sia una stringa e in List<Integer> nulla che non sia un intero. Ma non appena il programma parte, le differenze svaniscono. Per la JVM entrambi gli oggetti sono semplicemente ArrayList, e non può più verificare quali elementi dovrebbero essere memorizzati. Proprio per questo il confronto delle classi delle due liste (stringList.getClass() == intList.getClass()) restituisce true.
Ne deriva una conclusione importante: i generics in Java servono soprattutto per comodità e sicurezza in fase di compilazione. Ma a runtime queste «etichette» si perdono. Pertanto, se serializzate una collezione, nel file finiranno solo i dati, non l’informazione sui tipi generics. Cioè verrà salvata la lista dei valori, ma dal file non si può capire che fosse precisamente una List<String> e non una List<Object> o una List<Integer>.
Un altro esempio: serializzazione e deserializzazione di 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("Mela");
fruits.add("Banana");
fruits.add("Arancia");
// Serializzazione
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fruits.ser"))) {
oos.writeObject(fruits);
}
// Deserializzazione
List<String> loadedFruits;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fruits.ser"))) {
loadedFruits = (List<String>) ois.readObject();
}
System.out.println(loadedFruits); // [Mela, Banana, Arancia]
}
}
Prestate attenzione alla riga:
loadedFruits = (List<String>) ois.readObject();
Qui convertiamo esplicitamente il risultato al tipo List<String>, sebbene in realtà a runtime sia solo un ArrayList. Il compilatore non potrà verificare che sia davvero una lista di stringhe e, se dovessero finirci elementi non stringa, otterremo una ClassCastException durante l’esecuzione del programma.
2. Problemi durante la deserializzazione di collezioni generics
Perdita di informazioni sul tipo degli elementi
Poiché le informazioni sui parametri generics vengono cancellate, dopo la deserializzazione Java non può garantire che in una collezione ci siano proprio gli oggetti che vi aspettate. Tutto ciò che ottenete è una collezione «raw» (raw type) e il compilatore non dà errori, ma il problema può emergere a runtime.
Dimostrazione del problema
List rawList = new ArrayList();
rawList.add("Gatto");
rawList.add(42); // Integer!
// Deserializzazione
List<String> loadedCats = (List<String>) ois.readObject();
String cat = loadedCats.get(1); // ClassCastException!
Avviso di cast unchecked
Il compilatore avviserà correttamente del potenziale problema:
Note: GenericSerializationDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
Questo warning indica che state effettuando un cast senza verifica e che nella collezione potrebbero esserci oggetti di tipo inatteso.
3. Caratteristiche della serializzazione di collezioni generics
Nessuna informazione sui parametri generics nel file
Quando serializzate List<String> e List<Integer>, nel file non ci sarà alcuna informazione sul fatto che si trattasse di stringhe o numeri. Il contenuto della collezione viene serializzato «così com’è» — gli oggetti in ordine.
Se aprite il file serializzato in un editor di testo, non troverete alcuna traccia di <String> o <Integer>. Tutto ciò esiste solo a livello di codice sorgente e compilatore.
Esempio: serializzazione di collezioni diverse
List<Integer> numbers = Arrays.asList(1, 2, 3);
List<String> words = Arrays.asList("uno", "due", "tre");
// Serializzazione
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"))) {
oos.writeObject(numbers);
oos.writeObject(words);
}
Nel file test.ser ci sono semplicemente due oggetti di tipo ArrayList, senza alcuna informazione sui parametri generics.
Problema della deserializzazione di collezioni «raw»
Se avete serializzato una lista senza parametro generics (raw type) e la deserializzate come List<String>, il compilatore non potrà verificare la correttezza dei tipi e sono possibili errori a runtime.
4. Best practice per la serializzazione di collezioni generics
Documentate i tipi di elementi attesi.
Se il vostro API serializza una collezione, indicate sempre quale tipo di elementi è atteso. Ad esempio: «Questo metodo restituisce un List<User> serializzato».
Verificate i tipi degli elementi dopo la deserializzazione.
Dopo la deserializzazione di una collezione è utile controllare che tutti gli elementi abbiano il tipo atteso (soprattutto se la fonte dei dati non è sotto il vostro controllo).
for (Object obj : loadedList) {
if (!(obj instanceof String)) {
throw new IllegalStateException("Era attesa una stringa, ma trovato: " + obj.getClass());
}
}
Usate collezioni immutabili.
Se serializzate una collezione di sola lettura, usate collezioni immutabili — List.copyOf, Collections.unmodifiableList. Questo aiuterà a evitare modifiche accidentali dei dati dopo la deserializzazione.
Non mescolate tipi nella stessa collezione.
Evitate di serializzare collezioni con elementi di tipi diversi (per esempio, List<Object> con classi diverse al suo interno). Ciò complica la deserializzazione e può portare a errori.
Usate con cautela la soppressione degli avvisi.
Se siete certi di deserializzare una collezione con il tipo di elementi corretto, potete sopprimere l’avviso del compilatore con l’annotazione @SuppressWarnings("unchecked"):
@SuppressWarnings("unchecked")
List<String> loaded = (List<String>) ois.readObject();
Ma usatelo consapevolmente — è facile nascondere un problema fino alla produzione.
5. Esempio: serializzazione e deserializzazione di una collezione con una classe personalizzata
Supponiamo di avere la classe 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 + ")";
}
}
Serializziamo la lista di utenti:
List<User> users = Arrays.asList(
new User("Alisa", 30),
new User("Bob", 25)
);
// Serializzazione
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users.ser"))) {
oos.writeObject(users);
}
// Deserializzazione
List<User> loadedUsers;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users.ser"))) {
loadedUsers = (List<User>) ois.readObject();
}
System.out.println(loadedUsers); // [Alisa (30), Bob (25)]
Tutto funziona! Ma se qualcuno inserisce nel file serializzato un oggetto di un’altra classe, potreste ottenere una ClassCastException quando cercherete di leggere gli elementi come User.
6. Serializzazione di collezioni generics annidate
Le collezioni possono essere annidate, per esempio: List<List<String>>, Map<String, List<User>> ecc. Java serializza tali strutture in modo ricorsivo, ma le regole restano le stesse:
- Tutte le collezioni annidate e i relativi elementi devono essere serializzabili.
- L’informazione sui parametri generics continua a essere cancellata.
Esempio: serializzazione di una lista di liste
List<List<String>> matrix = new ArrayList<>();
matrix.add(Arrays.asList("a", "b"));
matrix.add(Arrays.asList("c", "d"));
// Serializzazione
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("matrix.ser"))) {
oos.writeObject(matrix);
}
// Deserializzazione
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. Dettagli utili
Serializzazione di collezioni generics con implementazioni diverse
Talvolta serializzate una certa implementazione di collezione e deserializzate come un’altra. Per esempio, avete serializzato un ArrayList e deserializzate come LinkedList. Questo porterà a un errore di cast:
List<String> list = new ArrayList<>();
// ...
List<String> loaded = (LinkedList<String>) ois.readObject(); // ClassCastException!
Consiglio: deserializzate sempre nello stesso tipo che avete serializzato oppure usate l’interfaccia (List) se l’implementazione concreta non vi interessa.
Uso di librerie (ad es., Gson, Jackson)
Le librerie per JSON (ad esempio, Gson, Jackson) sanno serializzare/deserializzare collezioni con generics, ma richiedono l’indicazione esplicita del tipo in deserializzazione a causa della cancellazione dei tipi. Esempio per Gson:
Type type = new com.google.gson.reflect.TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);
8. Generics e serializzazione in Map e Set
Tutte le regole di cui sopra valgono anche per altre collezioni con generics:
- Quando serializzate una Map<String, Integer>, l’informazione sui tipi di chiavi e valori non viene salvata.
- In deserializzazione bisogna convertire al tipo desiderato ed essere attenti al contenuto.
Esempio: serializzazione di una Map
Map<String, Integer> scores = new HashMap<>();
scores.put("Vasya", 90);
scores.put("Petya", 85);
// Serializzazione
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("scores.ser"))) {
oos.writeObject(scores);
}
// Deserializzazione
Map<String, Integer> loadedScores;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("scores.ser"))) {
loadedScores = (Map<String, Integer>) ois.readObject();
}
System.out.println(loadedScores); // {Vasya=90, Petya=85}
9. Errori tipici nella serializzazione di collezioni generics
Errore n. 1: ClassCastException durante la deserializzazione. Se deserializzate una collezione come List<String> ma al suo interno c’è un oggetto di un altro tipo, otterrete una ClassCastException a runtime. Controllate sempre il contenuto della collezione!
Errore n. 2: NotSerializableException a causa di un elemento non serializzabile. Se almeno un elemento della collezione non implementa Serializable, la serializzazione terminerà con NotSerializableException. Verificate la serializzabilità di tutte le classi che possono finire nella collezione.
Errore n. 3: Perdita di informazione sui parametri generics. Dopo la deserializzazione non fate affidamento sui parametri generics — a runtime non esistono. Usate verifiche esplicite dei tipi se avete dubbi sulla correttezza dei dati.
Errore n. 4: Mancata corrispondenza delle implementazioni delle collezioni. Avete serializzato un ArrayList ma deserializzate come LinkedList — otterrete un errore di cast. Cercate di deserializzare nello stesso tipo che è stato serializzato.
Errore n. 5: Incompatibilità delle versioni delle classi. Se la struttura della classe degli elementi della collezione è cambiata dopo la serializzazione (per esempio, è stato aggiunto un campo), sono possibili errori in deserializzazione. Usate serialVersionUID per il controllo delle versioni.
GO TO FULL VERSION