1. Introduzione
In Java per serializzare gli oggetti si usa più spesso l’interfaccia Serializable. È semplice: basta implementare l’interfaccia e l’oggetto si può scrivere/leggere tramite ObjectOutputStream/ObjectInputStream. Ma a volte non è sufficiente:
- Serve controllare completamente quali campi vengono serializzati e come.
- È necessario garantire la compatibilità tra diverse versioni della classe.
- È importante ridurre la dimensione del file serializzato o velocizzare il processo.
Per questi casi in Java esiste l’interfaccia Externalizable — un approccio più “manuale” e flessibile alla serializzazione.
In breve:
- Serializable — serializzazione automatica: è Java a decidere cosa e come scrivere.
- Externalizable — serializzazione manuale: sei tu a indicare cosa e come salvare/ripristinare.
2. Contratto Externalizable: implementiamo writeExternal e readExternal
Per usare Externalizable, occorre:
- Implementare l’interfaccia java.io.Externalizable.
- Implementare obbligatoriamente due metodi:
- void writeExternal(ObjectOutput out) throws IOException
- void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
Esempio:
import java.io.*;
public class User implements Externalizable {
private String name;
private int age;
// Costruttore pubblico senza argomenti obbligatorio!
public User() {}
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
Importante: lo sviluppatore decide in autonomia quali campi serializzare e in quale ordine. C’è però un requisito obbligatorio: la classe deve avere un costruttore public senza parametri. Se manca, durante la deserializzazione il programma genererà InvalidClassException.
3. Quando usare Externalizable?
Usa Externalizable se:
- Hai bisogno di un controllo completo sul formato dei dati. Ad esempio, vuoi serializzare solo una parte dei campi oppure serializzarli in un ordine/formato particolare.
- Ottimizzazione di prestazioni e dimensione del file. La serializzazione standard aggiunge informazioni di servizio (metadati, nomi delle classi, tipi, ecc.). Con Externalizable scrivi solo i dati necessari.
- Garanzia di retrocompatibilità. Se la struttura della classe cambia, puoi implementare manualmente la logica di lettura delle versioni vecchie e nuove dei dati.
- Serializzazione di oggetti non standard. Per esempio, se hai campi che non si possono serializzare con il metodo standard (ad esempio transient, volatile o strutture complesse).
Quando NON conviene usarlo?
- Se non ti serve un controllo completo — usa Serializable, è più semplice e sicuro.
- Se non sei sicuro di poter mantenere la compatibilità del formato dei dati quando la classe cambia.
4. Pro e contro di Externalizable rispetto a Serializable
Vantaggi:
- Controllo completo sulla serializzazione. Decidi tu cosa e come scrivere/leggere.
- Compattezza. Niente metadati superflui — solo i tuoi dati.
- Velocità. Meno dati — scrittura/lettura più rapida.
- Flessibilità. Puoi implementare il supporto di diverse versioni del formato, aggiungere compressione, cifratura, ecc.
Svantaggi:
- Implementazione manuale — è facile sbagliare. Se inverti l’ordine di scrittura/lettura, la serializzazione “si rompe” (errore o dati non corretti).
- Nessun supporto automatico per transient, serialVersionUID. Va pensato e implementato tutto a mano.
- Manutenzione più complessa. Se cambia la struttura della classe, non dimenticare di aggiornare i metodi di serializzazione.
- Costruttore pubblico senza parametri obbligatorio.
- Meno “magia” — più responsabilità.
5. Esempi: serializzazione e deserializzazione di un oggetto semplice
Serializzazione dell’oggetto
User user = new User("Alice", 30);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(user);
}
Deserializzazione dell’oggetto
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User loaded = (User) in.readObject();
System.out.println(loaded); // Alice (30)
}
Attenzione: se cambi l’ordine di scrittura/lettura dei campi o dimentichi di serializzare un campo, i dati saranno incorretti! I metodi writeExternal e readExternal devono essere strettamente allineati nella sequenza delle operazioni.
Esempio: serializziamo solo una parte dei campi
public class SecretUser implements Externalizable {
private String login;
private transient String password; // transient non ha rilevanza per Externalizable
public SecretUser() {}
public SecretUser(String login, String password) {
this.login = login;
this.password = password;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(login);
// Non serializziamo la password!
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
login = in.readUTF();
password = null; // la password non viene ripristinata
}
}
6. Pratica: confronto della dimensione del file serializzato
Confrontiamo quanto “pesano” i file serializzati con Serializable e con Externalizable.
Classe con Serializable
public class UserSerializable implements Serializable {
private String name;
private int age;
public UserSerializable(String name, int age) {
this.name = name;
this.age = age;
}
}
Classe con Externalizable
public class UserExternalizable implements Externalizable {
private String name;
private int age;
public UserExternalizable() {}
public UserExternalizable(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
}
}
Codice per il confronto
import java.io.*;
public class CompareSerialization {
public static void main(String[] args) throws Exception {
UserSerializable s = new UserSerializable("Bob", 25);
UserExternalizable e = new UserExternalizable("Bob", 25);
// Serializable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
out.writeObject(s);
}
// Externalizable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ext.bin"))) {
out.writeObject(e);
}
System.out.println("Serializable file size: " + new File("ser.bin").length());
System.out.println("Externalizable file size: " + new File("ext.bin").length());
}
}
Risultato:
Il file ser.bin (Serializable) di solito è più grande — contiene informazioni di servizio di Java. Il file ext.bin (Externalizable) — solo i tuoi dati, di norma è più piccolo.
7. Errori tipici nell’uso di Externalizable
Errore n. 1: assenza di un costruttore pubblico senza argomenti.
La classe che implementa Externalizable deve avere obbligatoriamente un costruttore public senza argomenti. In caso contrario, la deserializzazione genererà InvalidClassException.
Errore n. 2: violazione dell’ordine di scrittura e lettura dei campi.
I metodi writeExternal e readExternal devono operare nello stesso ordine. Se in scrittura salvi prima il campo name e in lettura provi prima a leggere age, i dati risulteranno corrotti.
Errore n. 3: campi omessi durante la serializzazione.
Se dimentichi di scrivere un campo in writeExternal, in deserializzazione avrà valore null (per i tipi reference) o 0 (per i numerici).
Errore n. 4: uso scorretto di transient o serialVersionUID.
A differenza di Serializable, per Externalizable questi meccanismi non funzionano automaticamente — devi controllare tu quali campi salvare e quali no.
Errore n. 5: modifica della struttura della classe senza aggiornare i metodi.
Se aggiungi o rimuovi campi e non apporti i cambiamenti corrispondenti a writeExternal e readExternal, i dati salvati in passato potrebbero non caricarsi correttamente.
GO TO FULL VERSION