1. L'interfaccia Serializable
Ricordi gli esempi della lezione precedente? Le classi Java che vogliamo serializzare devono implementare una speciale interfaccia — java.io.Serializable. Si tratta della cosiddetta interfaccia marker: non contiene alcun metodo, ma semplicemente «etichetta» la classe come adatta alla serializzazione. Se una classe implementa questa interfaccia, la JVM consente di serializzare i suoi oggetti con i mezzi standard.
Non è consigliabile serializzare tutto indiscriminatamente, perché non tutti gli oggetti si possono o si devono salvare in byte. Alcuni oggetti dipendono dallo stato del sistema operativo, da file aperti o da connessioni di rete. Per questo Java richiede di contrassegnare esplicitamente una classe come serializzabile.
Un'interfaccia marker è come un adesivo «consentito all'imballaggio» su una scatola. Se l'adesivo non c'è — l'imballatore (la JVM) si rifiuta di lavorare.
Esempio: dichiarazione di una classe serializzabile
import java.io.Serializable;
public class User implements Serializable {
private String name;
private int age;
// Costruttore, getter e setter
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Per comodità: metodo toString()
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
Nota bene:
- Abbiamo semplicemente aggiunto implements Serializable alla dichiarazione della classe.
- Non è necessario implementare alcun metodo (l'interfaccia è vuota).
- Tutte le classi standard di Java che si possono serializzare (ad esempio, ArrayList, HashMap, String) implementano già Serializable.
2. Come rendere serializzabile la propria classe
Regola n. 1: basta aggiungere implements Serializable
Questo è tutto ciò che serve per la classe in sé. Ma ci sono dei dettagli!
Importante: anche tutti gli oggetti annidati devono essere serializzabili.
Se la tua classe ha campi che sono riferimenti ad altri oggetti, anche questi devono essere serializzabili. Ad esempio:
public class Profile implements Serializable {
private User user; // User deve essere serializzabile!
private int level;
}
Se almeno uno dei campi non è serializzabile, al tentativo di serializzazione verrà generata un'eccezione.
3. Esempio di serializzazione e deserializzazione
Vediamo come serializzare e deserializzare un oggetto su file. Per questo si usano le classi ObjectOutputStream e ObjectInputStream.
Esempio: serializzazione di un oggetto User su file
import java.io.*;
public class SerializeDemo {
public static void main(String[] args) {
User user = new User("Alice", 30);
// Salviamo l'oggetto su file
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("Oggetto serializzato correttamente nel file user.ser");
} catch (IOException e) {
System.out.println("Errore di serializzazione: " + e.getMessage());
}
}
}
Cosa succede qui?
- Creiamo un oggetto User.
- Apriamo uno stream ObjectOutputStream che scrive nel file "user.ser".
- Chiamiamo writeObject(user). In questo momento la JVM converte l'oggetto in un flusso di byte e lo salva nel file.
Esempio: deserializzazione di un oggetto da file
import java.io.*;
public class DeserializeDemo {
public static void main(String[] args) {
// Leggiamo l'oggetto dal file
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User user = (User) ois.readObject();
System.out.println("Oggetto ripristinato con successo: " + user);
} catch (IOException | ClassNotFoundException e) {
System.out.println("Errore di deserializzazione: " + e.getMessage());
}
}
}
Cosa succede qui?
- Apriamo uno stream ObjectInputStream che legge dal file "user.ser".
- Chiamiamo readObject(). La JVM ricostruisce l'oggetto dai byte.
- Non dimenticare di effettuare il cast al tipo necessario (User), perché readObject() restituisce Object.
- Può verificarsi ClassNotFoundException se la classe User non viene trovata durante la deserializzazione.
Tutto insieme: serializzazione e deserializzazione
import java.io.*;
public class SerializationExample {
public static void main(String[] args) {
User user = new User("Bob", 22);
// Serializzazione
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("Serializzazione completata!");
} catch (IOException e) {
System.out.println("Errore di serializzazione: " + e.getMessage());
}
// Deserializzazione
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User loaded = (User) ois.readObject();
System.out.println("Deserializzazione completata! " + loaded);
} catch (IOException | ClassNotFoundException e) {
System.out.println("Errore di deserializzazione: " + e.getMessage());
}
}
}
Risultato:
Serializzazione completata!
Deserializzazione completata! User{name='Bob', age=22}
4. Cosa succede «sotto il cofano» durante la serializzazione
Quando chiami writeObject, la JVM verifica innanzitutto se la classe implementa l'interfaccia Serializable. Se la classe non è contrassegnata come serializzabile, viene lanciata un'eccezione. Poi la JVM scorre tutti i campi ordinari dell'oggetto (cioè quelli che non sono static e non transient) e ne scrive i valori nel flusso di byte. Se tra questi campi si trovano altri oggetti, la serializzazione si applica loro ricorsivamente, ma solo se anch'essi implementano Serializable.
Durante la deserializzazione l'oggetto viene creato senza chiamare il costruttore normale, e i suoi campi vengono riempiti con i valori salvati — come se un «costruttore senza costruttore» riportasse in vita l'oggetto dal flusso di byte.
Alcuni campi non verranno serializzati. I campi static appartengono alla classe e non al singolo oggetto, quindi i loro valori non vengono salvati. I campi contrassegnati come transient vengono anch'essi saltati — è utile per dati temporanei, cache o informazioni riservate come le password.
Schema del processo di serializzazione
flowchart TB
A[Oggetto User in memoria] -- writeObject --> B[ObjectOutputStream]
B -- salva i byte --> C[File user.ser]
C -- readObject --> D[ObjectInputStream]
D -- ricostruisce --> E[Oggetto User in memoria]
5. Errori tipici nell'uso di Serializable
Errore n. 1: campo riferimento a un oggetto non serializzabile. Se nella classe User compare un campo di tipo, per esempio, Thread o Socket, la serializzazione non funzionerà. Non tutti gli oggetti possono essere serializzati — ricordalo!
Errore n. 2: classi annidate non serializzabili. Se la classe User contiene una classe interna che non è static, la serializzazione potrebbe non funzionare. È meglio usare classi annidate static o classi separate in file dedicati.
Errore n. 3: tentativo di serializzare un static campo. I campi statici non vengono serializzati — appartengono alla classe, non all'oggetto. Dopo la deserializzazione, il static campo avrà il valore definito nella classe, non nell'oggetto serializzato.
Errore n. 4: disallineamento delle versioni della classe. Se modifichi la struttura della classe dopo la serializzazione (per esempio aggiungendo o rimuovendo un campo) e poi provi a deserializzare il vecchio oggetto, può verificarsi l'errore InvalidClassException. Per il controllo delle versioni si usa il campo speciale serialVersionUID — ne parleremo più in dettaglio nella prossima lezione.
GO TO FULL VERSION