1. Introduzione: serializzazione di oggetti con campi di riferimento
Nei progetti reali si incontrano di rado classi completamente “piatte”. Di solito un oggetto contiene altri oggetti, che a loro volta possono contenerne altri. Questo si chiama composizione (o annidamento) di oggetti. Per esempio:
public class Address {
String city;
String street;
}
public class Person {
String name;
int age;
Address address; // Oggetto annidato!
}
Quando serializziamo un oggetto del genere, sorge una domanda: cosa fare con il campo address? Java deve serializzarlo insieme a Person? E se dentro Address ci fosse a sua volta un altro oggetto? Per fortuna (o sfortuna — dipende dalla situazione), Java per impostazione predefinita serializza ricorsivamente tutti gli oggetti annidati, se anch’essi implementano l’interfaccia Serializable.
La serializzazione in Java è sempre una serializzazione profonda (deep serialization). Significa che viene serializzato non solo l’oggetto stesso, ma anche tutti gli oggetti a cui esso fa riferimento tramite i propri campi (non transient), e così via — fino al “fondo”.
Visualizzazione del processo
graph TD
A[Person] --> B[Address]
B --> C[CityInfo]
A --> D[Pet]
In breve, se decidete di serializzare Person, Java serializzerà anche Address e tutto ciò che è all’interno di Address, e così via.
2. Requisiti per gli oggetti annidati: Serializable è obbligatorio!
Probabilmente avete già notato un punto importante: tutti gli oggetti annidati che vengono serializzati devono anch’essi implementare l’interfaccia Serializable.
Se almeno un campo di riferimento punta a un oggetto che non implementa Serializable, il tentativo di serializzazione terminerà con l’eccezione java.io.NotSerializableException.
Vediamo alcuni esempi di serializzazione profonda.
Esempio: tutto bene
import java.io.Serializable;
public class Address implements Serializable {
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Entrambe le classi implementano Serializable. Tutto funziona, la serializzazione va a buon fine.
Esempio: errore!
public class Address { // NON implementa Serializable!
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Come vediamo, qui Address non implementa l’interfaccia Serializable. Quindi durante la serializzazione di Person otterremo l’eccezione NotSerializableException.
3. Esempio: serializzazione e deserializzazione con oggetto annidato
Vediamo come appare nella pratica. Al livello precedente abbiamo lavorato con l’applicazione “Gestore contatti”. Aggiungiamo ora l’indirizzo di ogni utente.
import java.io.*;
class Address implements Serializable {
String city;
String street;
Address(String city, String street) {
this.city = city;
this.street = street;
}
}
class Person implements Serializable {
String name;
int age;
Address address;
Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
}
public class SerializationDemo {
public static void main(String[] args) throws Exception {
Person p = new Person("Ivan", 30, new Address("Praga", "Slavinska, 1"));
// Serializzazione
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Deserializzazione
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name + ", " + restored.age + ", " +
restored.address.city + ", " + restored.address.street);
}
}
Risultato:
Ivan, 30, Praga, Slavinska, 1
Funziona tutto: l’oggetto annidato Address è stato serializzato e ripristinato insieme a Person.
4. E se l’oggetto annidato non è serializzabile?
Se provate a serializzare un oggetto che ha almeno un campo di riferimento verso un oggetto che non implementa Serializable, Java lancerà un’eccezione già al tentativo di serializzazione.
Dimostrazione dell’errore
class Address { // non Serializable!
String city;
String street;
}
class Person implements Serializable {
String name;
Address address;
}
public class Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.name = "Petya";
p.address = new Address();
p.address.city = "Derry";
p.address.street = "Vyazov";
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p); // <-- CI SARÀ UN'ECCEZIONE!
out.close();
}
}
Errore:
java.io.NotSerializableException: Address
5. Transient per gli oggetti annidati
Cosa fare se avete un campo di riferimento a un oggetto che non deve essere serializzato (per esempio, una cache, una connessione al database, un oggetto temporaneo)? In tal caso dichiarate il campo come transient. Java ignorerà semplicemente questo campo durante la serializzazione.
Esempio con transient
class Address { // non Serializable
String city;
String street;
}
class Person implements Serializable {
String name;
transient Address address; // transient!
Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
public class Test {
public static void main(String[] args) throws Exception {
Address addr = new Address();
addr.city = "Los Santos";
addr.street = "Mulholland Drive";
Person p = new Person("Sasha", addr);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Deserializzazione
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // "Sasha"
System.out.println(restored.address); // null!
}
}
Risultato:
Il campo address dopo la deserializzazione è pari a null, perché è transient.
6. Annidamento profondo e ricorsione
La serializzazione funziona in modo ricorsivo: se Person ha un campo Address, e Address ha un campo CityInfo, e così via, il serializzatore “scenderà” sempre più in profondità, finché non incontra qualcosa di non serializzabile o finché non finisce la memoria (battuta, ma solo a metà).
Importante: riferimenti ciclici
Il serializzatore di Java sa gestire i riferimenti ciclici. Se, per esempio, un oggetto ha un riferimento a un altro oggetto che a sua volta rimanda al primo, il serializzatore non andrà in loop, ma salverà correttamente la struttura.
class A implements Serializable {
B b;
}
class B implements Serializable {
A a;
}
Se create oggetti A e B che si rimandano a vicenda, la serializzazione non causerà StackOverflowError — Java ricorda gli oggetti già serializzati.
7. Esempio: serializzazione di un oggetto con lista annidata
Spesso un oggetto contiene collezioni di altri oggetti. Per esempio, un utente può avere una lista di amici:
import java.io.*;
import java.util.*;
class Person implements Serializable {
String name;
List<Person> friends;
Person(String name) {
this.name = name;
this.friends = new ArrayList<>();
}
}
public class FriendsSerialization {
public static void main(String[] args) throws Exception {
Person alice = new Person("Alice");
Person bob = new Person("Bob");
alice.friends.add(bob);
// Serializzazione
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("friends.ser"));
out.writeObject(alice);
out.close();
// Deserializzazione
ObjectInputStream in = new ObjectInputStream(new FileInputStream("friends.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // Alice
System.out.println(restored.friends.get(0).name); // Bob
}
}
Importante: Le collezioni della libreria standard Java (ArrayList, HashMap ecc.) implementano Serializable, quindi tutto funziona “pronto all’uso”.
8. Errori tipici nella serializzazione di oggetti annidati
Errore n. 1: uno degli oggetti annidati non implementa Serializable. Vi siete dimenticati di aggiungere implements Serializable a una delle classi annidate. Risultato — NotSerializableException al primo tentativo di serializzazione. Verificate che tutte le classi nella vostra catena di serializzazione supportino questa interfaccia.
Errore n. 2: un campo non serializzabile non è dichiarato transient. Se avete un campo che non deve essere serializzato (per esempio, uno stream, una connessione al database, qualcosa di temporaneo) ma non lo dichiarate transient, la serializzazione fallirà con un errore. Non dimenticate transient!
Errore n. 3: mancata corrispondenza di serialVersionUID nelle classi annidate. Se dichiarate esplicitamente serialVersionUID nelle classi annidate e ne cambiate la struttura, ricordate di aggiornare questo identificatore — altrimenti sono possibili errori in fase di deserializzazione.
Errore n. 4: oggetti annidati mutabili (modificabili). Se serializzate una collezione o un oggetto che poi cambia (per esempio, la lista degli amici), dopo la deserializzazione avrete comunque una “istantanea” al momento della serializzazione. Le modifiche successive all’originale non si rifletteranno nell’oggetto deserializzato.
Errore n. 5: serializzazione di grafi di oggetti enormi. Se la vostra struttura è molto complessa e con molti oggetti annidati, la serializzazione può richiedere molto tempo e memoria. Talvolta è meglio serializzare solo i dati chiave, non l’intera struttura.
GO TO FULL VERSION