1. NotSerializableException: quando la collezione non vuole essere serializzata
L'errore più frequente e insidioso nella serializzazione delle collezioni è java.io.NotSerializableException. Si verifica se almeno un elemento della collezione non implementa l'interfaccia Serializable.
Vediamo un esempio ingenuo:
import java.io.*;
import java.util.*;
class Book {
String title;
Book(String title) { this.title = title; }
}
public class LibraryApp {
public static void main(String[] args) throws Exception {
List<Book> books = new ArrayList<>();
books.add(new Book("Dombey e figlio"));
// Tentativo di serializzare la collezione
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
oos.writeObject(books); // BOOM! NotSerializableException
}
}
}
Cosa succede? Al momento di oos.writeObject(books) otterrete un'eccezione:
java.io.NotSerializableException: Book
Perché? Perché la classe Book non implementa l'interfaccia Serializable. Anche se la collezione stessa (ArrayList) sa essere serializzata, gli elementi della collezione devono essere serializzabili!
Come diagnosticare
Nel messaggio di errore è sempre indicata la classe che ha causato il problema — cercatela nel testo dell'eccezione. Se la collezione è grande e l'errore si verifica solo in determinate condizioni, è possibile che un elemento sia stato aggiunto per sbaglio e non implementi Serializable.
Come risolvere
Aggiungete implements Serializable alla vostra classe:
class Book implements Serializable {
String title;
Book(String title) { this.title = title; }
}
Suggerimento: Se la collezione contiene tipi di oggetti diversi, verificate che tutti implementino Serializable!
2. ClassCastException durante la deserializzazione: quando i generics tradiscono
In Java le informazioni sui parametri generici delle collezioni vengono eliminate dopo la compilazione (type erasure). Ciò significa che se avete serializzato un List<String>, ma deserializzate come List<Integer>, il compilatore non rileverà l'errore, ma a runtime otterrete una ClassCastException.
Esempio:
// Serializzazione
List<String> names = Arrays.asList("Anna", "Boris");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("names.ser"))) {
oos.writeObject(names);
}
// Deserializzazione (PERICOLOSO!)
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("names.ser"))) {
List<Integer> numbers = (List<Integer>) ois.readObject(); // unchecked cast
Integer first = numbers.get(0); // BOOM! ClassCastException
}
Errore:
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
Come evitare
- Non usate collezioni "raw" (raw types) e non fate cast non necessari.
- Verificate i tipi degli elementi dopo la deserializzazione se non siete sicuri del loro contenuto.
- Documentate quale tipo di collezione viene serializzato e quale ci si aspetta in lettura.
Esempio di deserializzazione sicura:
Object obj = ois.readObject();
if (obj instanceof List<?>) {
List<?> list = (List<?>) obj;
if (!list.isEmpty() && list.get(0) instanceof String) {
@SuppressWarnings("unchecked")
List<String> safeNames = (List<String>) obj; // avviso soppresso, ma il tipo è stato verificato!
}
}
3. Modifica della struttura delle classi: serialVersionUID e compatibilità all'indietro
Avete serializzato una collezione e poi avete deciso di aggiungere un nuovo campo alla classe degli elementi, cambiare il nome di un campo o addirittura modificare la struttura della classe. Ora, tentando di deserializzare il vecchio file, otterrete un errore enigmatico:
java.io.InvalidClassException: Book; local class incompatible: stream classdesc serialVersionUID = 1234, local class serialVersionUID = 5678
Perché accade
Ogni classe serializzabile ha un identificatore di versione univoco — serialVersionUID. Se la classe è cambiata (ad esempio avete aggiunto un campo), la JVM calcola un nuovo serialVersionUID e, in deserializzazione, si vede che la versione della classe non coincide con quella usata al momento della serializzazione.
Come evitare
- Dichiarate esplicitamente serialVersionUID nelle vostre classi:
class Book implements Serializable {
private static final long serialVersionUID = 1L;
String title;
// ...
}
- Mantenete la compatibilità all'indietro: non eliminate e non rinominate campi se prevedete di leggere file più vecchi.
- Testate la deserializzazione dopo le modifiche.
Cosa fare se è comunque necessario modificare la classe?
- Valutate l'implementazione dei metodi readObject/writeObject per gestire manualmente la serializzazione.
- Oppure migrate i dati: leggete il vecchio file con la versione precedente della classe, poi risalvatelo nel nuovo formato.
4. Perdita di dati nella serializzazione delle collezioni immutabili
Nelle versioni più recenti di Java sono state introdotte collezioni immutabili, ad esempio create tramite List.of(), Set.of(), Map.of(). Nelle versioni più vecchie di Java (fino alla 12) e in alcune implementazioni di terze parti la serializzazione di tali collezioni può non funzionare correttamente: dopo la deserializzazione la collezione può diventare mutabile oppure si verifica un errore.
Esempio:
List<String> list = List.of("a", "b", "c");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
oos.writeObject(list);
}
Su JVM più vecchie, in fase di deserializzazione si verificava un errore oppure la collezione non risultava più immutabile.
Come evitare
- Controllate la documentazione della versione di Java in uso.
- Testate la serializzazione e deserializzazione di tali collezioni.
- Se dovete mantenere l'immutabilità, dopo la deserializzazione avvolgete la collezione con Collections.unmodifiableList(list).
5. Serializzazione dei campi transient e static
Cosa succede con questi campi:
- transient — i campi contrassegnati con questa parola chiave non vengono serializzati. Dopo la deserializzazione avranno il valore predefinito (ad esempio, null o 0).
- static — i campi di classe (e non dell'oggetto) non vengono mai serializzati.
Esempio:
class Book implements Serializable {
String title;
transient String cache; // non viene serializzato!
static String publisher = "Default"; // non viene serializzato neanche questo!
}
Perché è importante
Se conservate valori calcolati o una cache all'interno dell'oggetto, contrassegnateli come transient: si risparmia spazio e si velocizza la serializzazione.
Attenzione: Dopo la deserializzazione, i campi transient devono essere ricalcolati o inizializzati di nuovo.
6. Serializzazione di collezioni grandi: prestazioni e dimensione del file
Problemi:
- Le collezioni molto grandi (ad esempio, un milione di oggetti) possono produrre file enormi, tempi di scrittura e lettura molto lunghi e talvolta anche esaurimento della memoria (OutOfMemoryError).
- Serializzando un grafo di oggetti (ad esempio collezioni complesse e interconnesse) la dimensione del file può crescere in modo inatteso.
Come evitare
- Serializzate la collezione a pezzi: ad esempio, scrivete gli oggetti uno per volta o in piccoli lotti.
- Usate l'elaborazione in streaming: invece di serializzare l'intera collezione in una volta, serializzate gli elementi man mano che servono.
- Comprimete i file: utilizzate GZIPOutputStream per ridurre la dimensione del file.
Esempio di serializzazione in streaming:
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
for (Book book : bigList) {
oos.writeObject(book);
}
}
Attenzione: Con questo approccio, per deserializzare è necessario sapere quanti oggetti sono stati scritti (oppure usare un apposito "marcatore di fine").
GO TO FULL VERSION