1. Introduzione
La serializzazione automatica — è come il pilota automatico in aereo: funziona alla grande finché tutto procede secondo i piani. Ma quando compaiono condizioni particolari, diventa chiaro che il semplice meccanismo non basta più. Immaginate di dover salvare un oggetto, ma non tutti i suoi campi: alcuni dati sono temporanei, altri troppo sensibili per essere scritti su file. Oppure — al momento del salvataggio — serve aggiungere qualcosa di proprio: ad esempio una versione o un checksum. Capita anche che, prima della scrittura o del caricamento dei dati, occorra eseguire una verifica o una trasformazione. E talvolta il compito è ancora più complesso: garantire la compatibilità con le versioni precedenti della classe, se la sua struttura è cambiata nel tempo.
In queste situazioni diventa evidente: la sola serializzazione standard non basta. Bisogna prendere il controllo nelle proprie mani.
Metodi speciali di serializzazione: writeObject e readObject
In Java ci sono due metodi speciali che consentono di controllare completamente il processo di serializzazione e deserializzazione di un oggetto:
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
Importante!
- I metodi devono essere private (non public, non protected, non package-private).
- Le firme devono corrispondere esattamente a quelle riportate sopra.
- Se questi metodi sono dichiarati nella vostra classe, verranno chiamati al posto della serializzazione/deserializzazione standard.
Come funziona?
Quando chiamate ObjectOutputStream.writeObject(obj), la JVM cerca innanzitutto nella classe di obj il metodo private void writeObject(ObjectOutputStream). Se esiste — viene chiamato proprio quello. Analogamente, in deserializzazione viene chiamato private void readObject(ObjectInputStream).
Se i metodi non sono dichiarati, verrà utilizzata la serializzazione standard.
Come sono fatti writeObject e readObject
Firme dei metodi
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
All'interno di questi metodi dovete chiamare:
- out.defaultWriteObject(); — per serializzare i campi standard (non transient) della superclasse e della classe corrente.
- in.defaultReadObject(); — per deserializzare i campi standard.
Se non chiamate questi metodi, i campi standard non verranno serializzati — e durante la deserializzazione l'oggetto risulterà «vuoto». È come dimenticare di mettere il passaporto in valigia: formalmente siete arrivati, ma non potrete dimostrare chi siete.
2. Esempio: aggiungere un checksum durante la serializzazione
Vediamo un esempio pratico. Supponiamo di avere una classe utente e di voler aggiungere all'oggetto un checksum in fase di serializzazione, per verificarne l'integrità in deserializzazione.
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// campo transient — non viene serializzato
private transient int checksum;
public User(String name, int age) {
this.name = name;
this.age = age;
this.checksum = calculateChecksum();
}
private int calculateChecksum() {
return (name != null ? name.hashCode() : 0) + age;
}
// Serializzazione personalizzata
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // Salviamo i campi standard
int sum = calculateChecksum();
out.writeInt(sum); // Scriviamo il checksum
System.out.println("[LOG] Serializzazione User: checksum=" + sum);
}
// Deserializzazione personalizzata
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Ripristiniamo i campi standard
int sum = in.readInt(); // Leggiamo il checksum
int actual = calculateChecksum();
System.out.println("[LOG] Deserializzazione User: checksum=" + sum + ", effettivo=" + actual);
if (sum != actual) {
throw new IOException("Dati danneggiati! Il checksum non corrisponde.");
}
this.checksum = actual;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", checksum=" + checksum + "}";
}
}
Esempio di utilizzo:
// Salviamo l'oggetto
User user = new User("Alice", 42);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(user);
}
// Carichiamo l'oggetto
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User loaded = (User) in.readObject();
System.out.println("Oggetto ripristinato: " + loaded);
}
Cosa succede?
- In serializzazione viene chiamato writeObject, vengono salvati i campi standard + il checksum.
- In deserializzazione viene chiamato readObject, vengono ripristinati i campi + verificato il checksum.
- In console comparirà un log e, se qualcosa non va, verrà lanciata un'eccezione.
3. Escludere dati sensibili dalla serializzazione
Talvolta è necessario che determinati campi non vengano serializzati (ad esempio, le password). Per questo si può usare la parola chiave transient (ne parleremo più in dettaglio nella prossima lezione), ma si può anche evitare di serializzare il campo manualmente, se implementate writeObject.
Esempio:
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient — non viene serializzato
// Ma si può anche fare così:
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// Non scriviamo password!
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// password rimane null
}
}
Attenzione:
Se volete serializzare solo una parte dell'oggetto — semplicemente non scrivete nel flusso i campi superflui.
4. Chiamata dei metodi della superclasse: defaultWriteObject e defaultReadObject
All'interno dei vostri metodi writeObject e readObject è quasi sempre necessario chiamare defaultWriteObject() e defaultReadObject(). È come premere «salva bozza» prima di aggiungere le vostre note.
Questi metodi si occupano della serializzazione standard di tutti i campi non-transient, non-static della classe corrente e della superclasse. Se non vengono chiamati, tali campi non verranno serializzati e, in deserializzazione, risulteranno vuoti.
Esempio di comportamento errato:
private void writeObject(ObjectOutputStream out) throws IOException {
// out.defaultWriteObject(); // ci siamo dimenticati di chiamarlo!
out.writeInt(123); // qualcosa di nostro
}
In questo caso i campi standard semplicemente non verranno salvati!
5. Pratica: logging del processo di serializzazione
Aggiungiamo il logging alla nostra classe Person per vedere quando avvengono serializzazione e deserializzazione.
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("[LOG] Serializzazione Person: " + name + ", età " + age);
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
System.out.println("[LOG] Deserializzazione Person: " + name + ", età " + age);
}
}
Utilizzo:
Person p = new Person("Bob", 30);
// Salviamo su file
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
out.writeObject(p);
}
// Carichiamo dal file
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"))) {
Person loaded = (Person) in.readObject();
}
Risultato:
In console vedrete messaggi che indicano che l'oggetto viene serializzato e deserializzato.
6. Errori tipici nell'uso di writeObject/readObject
Errore n. 1: defaultWriteObject/defaultReadObject non chiamati. Se ci si dimentica di chiamare questi metodi, i campi standard non verranno serializzati e l'oggetto, dopo la deserializzazione, risulterà vuoto o non corretto.
Errore n. 2: firma dei metodi errata. I metodi devono essere esattamente private void writeObject(ObjectOutputStream) e private void readObject(ObjectInputStream). Se li rendete public/protected o ne modificate i parametri — non verranno chiamati automaticamente.
Errore n. 3: eccezione nel metodo. Se in writeObject o readObject si verifica un'eccezione, la serializzazione o la deserializzazione verrà interrotta e l'oggetto non sarà salvato/caricato correttamente.
Errore n. 4: serializzazione/deserializzazione della superclasse dimenticata. Se la vostra classe estende un'altra classe serializzabile, chiamate obbligatoriamente defaultWriteObject/defaultReadObject, altrimenti i campi della superclasse non verranno salvati.
Errore n. 5: serializzazione di dati sensibili. Se vi dimenticate di escludere password o altri dati privati, finiranno nel file serializzato. Usate transient oppure non serializzateli manualmente.
GO TO FULL VERSION