1. Serializzazione di collezioni dentro collezioni
In Java le collezioni possono contenere non solo tipi semplici (ad esempio String), ma anche altre collezioni o oggetti. Questo apre le porte alla creazione di strutture complesse: ad esempio, Map<String, List<User>>, dove User è una classe definita da te.
Esempio: serializzazione di una Map con List annidato
Consideriamo un esempio di un piccolo social network, in cui ogni utente ha un elenco di amici.
import java.io.*;
import java.util.*;
class User implements Serializable {
private static final long serialVersionUID = 1L;
String name;
User(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" + "name='" + name + '\'' + '}';
}
}
public class SocialNetwork implements Serializable {
private static final long serialVersionUID = 1L;
Map<String, List<User>> friends = new HashMap<>();
public static void main(String[] args) throws IOException, ClassNotFoundException {
SocialNetwork network = new SocialNetwork();
network.friends.put("alice", Arrays.asList(new User("bob"), new User("carol")));
network.friends.put("bob", Collections.singletonList(new User("alice")));
// Serializzazione
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("network.ser"))) {
out.writeObject(network);
}
// Deserializzazione
SocialNetwork loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("network.ser"))) {
loaded = (SocialNetwork) in.readObject();
}
System.out.println("Rete ripristinata: " + loaded.friends);
}
}
Si può dire così: tutti i tipi utilizzati — che si tratti di HashMap, ArrayList o User — implementano l’interfaccia Serializable. Durante la serializzazione Java attraversa automaticamente tutte le collezioni e gli oggetti annidati, scrivendo anche questi. Pertanto, dopo la deserializzazione ottieni una struttura completamente ripristinata, inclusi tutti gli elenchi annidati.
Output:
Rete ripristinata: {alice=[User{name='bob'}, User{name='carol'}], bob=[User{name='alice'}]}
Annidamento a piacere
Puoi creare quanti livelli di annidamento desideri: List<List<User>>, Map<String, Map<Integer, List<User>>> — Java non ha paura della ricorsione (entro limiti ragionevoli, ovviamente).
2. Oggetti gerarchici: serializzazione di collezioni con ereditarietà
E se le tue collezioni contenessero oggetti costruiti secondo il principio dell’ereditarietà? Ad esempio, hai una classe base Animal e nella collezione ci sono sia Cat che Dog?
Esempio: serializzazione di una collezione con sottoclassi
import java.io.*;
import java.util.*;
abstract class Animal implements Serializable {
private static final long serialVersionUID = 1L;
String name;
Animal(String name) {
this.name = name;
}
public abstract String speak();
}
class Cat extends Animal {
private static final long serialVersionUID = 1L;
Cat(String name) {
super(name);
}
@Override
public String speak() {
return "Meow!";
}
}
class Dog extends Animal {
private static final long serialVersionUID = 1L;
Dog(String name) {
super(name);
}
@Override
public String speak() {
return "Woof!";
}
}
public class Zoo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
List<Animal> animals = new ArrayList<>();
animals.add(new Cat("Murka"));
animals.add(new Dog("Sharik"));
// Serializzazione
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("zoo.ser"))) {
out.writeObject(animals);
}
// Deserializzazione
List<Animal> loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("zoo.ser"))) {
loaded = (List<Animal>) in.readObject();
}
for (Animal animal : loaded) {
System.out.println(animal.name + " dice: " + animal.speak());
}
}
}
Risultato:
Murka dice: Meow!
Sharik dice: Woof!
Punto importante: Java serializza non solo i campi della classe base, ma anche le informazioni sul tipo reale dell’oggetto. Perciò, dopo la deserializzazione gli oggetti conservano la loro natura «felina» o «canina», e puoi chiamarne i metodi in sicurezza.
3. Serializzazione di grafi di oggetti
Ora è il momento di passare alla vera magia — la serializzazione di grafi di oggetti, dove gli oggetti possono fare riferimento l’uno all’altro e non solo essere annidati. Ma prima capiamo che cosa sono questi grafi.
Che cos’è un grafo di oggetti?
Un grafo di oggetti è una struttura in cui gli oggetti possono essere collegati tra loro tramite campi-riferimenti. Ad esempio, in un albero genealogico ogni persona può avere riferimenti a genitori, figli, fratelli e sorelle.
Analogia: Immagina un gruppo di amici in un social network: ogni utente ha un elenco di amici, e questi amici sono a loro volta utenti con i propri amici, e così via. Questo è un grafo di oggetti.
Esempio: serializzazione di una lista doppiamente collegata
import java.io.*;
class Node implements Serializable {
private static final long serialVersionUID = 1L;
String value;
Node next;
Node prev;
Node(String value) {
this.value = value;
}
}
public class DoublyLinkedListDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// Creiamo due nodi collegati
Node first = new Node("A");
Node second = new Node("B");
first.next = second;
second.prev = first;
// Serializzazione
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
out.writeObject(first);
}
// Deserializzazione
Node loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("list.ser"))) {
loaded = (Node) in.readObject();
}
System.out.println("Valore del primo: " + loaded.value); // "A"
System.out.println("Successivo: " + loaded.next.value); // "B"
System.out.println("Precedente del successivo: " + loaded.next.prev.value); // "A"
}
}
Nota che viene serializzato un solo riferimento (first), ma grazie alla serializzazione ricorsiva Java «percorrerà» tutti gli oggetti collegati. In deserializzazione la struttura dei riferimenti verrà completamente ripristinata: loaded.next.prev == loaded sarà true! E se nel grafo ci sono cicli (ad esempio quando i nodi puntano l’uno all’altro), la serializzazione standard di Java funziona correttamente e non va in loop.
4. Collezioni annidate e gerarchiche: esempio con una classe reale
Modello: catalogo di libri
Supponiamo di avere la classe Book, che può essere un libro cartaceo o un’edizione elettronica (ereditarietà). C’è anche la classe Library, che contiene una mappa di generi (Map<String, List<Book>>). Ogni genere è un elenco di libri.
import java.io.*;
import java.util.*;
abstract class Book implements Serializable {
private static final long serialVersionUID = 1L;
String title;
Book(String title) {
this.title = title;
}
}
class PaperBook extends Book {
private static final long serialVersionUID = 1L;
int pages;
PaperBook(String title, int pages) {
super(title);
this.pages = pages;
}
}
class EBook extends Book {
private static final long serialVersionUID = 1L;
String format;
EBook(String title, String format) {
super(title);
this.format = format;
}
}
class Library implements Serializable {
private static final long serialVersionUID = 1L;
Map<String, List<Book>> catalog = new HashMap<>();
}
public class CatalogDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Library library = new Library();
library.catalog.put("Fantascienza", Arrays.asList(
new PaperBook("Dune", 800),
new EBook("Il marziano", "epub")
));
library.catalog.put("Classici", Collections.singletonList(
new PaperBook("Guerra e pace", 1200)
));
// Serializzazione
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("library.ser"))) {
out.writeObject(library);
}
// Deserializzazione
Library loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("library.ser"))) {
loaded = (Library) in.readObject();
}
for (Map.Entry<String, List<Book>> entry : loaded.catalog.entrySet()) {
System.out.println("Genere: " + entry.getKey());
for (Book book : entry.getValue()) {
System.out.println(" - " + book.title + " (" + book.getClass().getSimpleName() + ")");
}
}
}
}
Output:
Genere: Fantascienza
- Dune (PaperBook)
- Il marziano (EBook)
Genere: Classici
- Guerra e pace (PaperBook)
In sintesi:
- La serializzazione delle collezioni annidate (Map<String, List<Book>>) funziona «pronta all’uso».
- I tipi degli oggetti (PaperBook, EBook) vengono preservati.
- Dopo la deserializzazione la struttura viene completamente ripristinata.
5. Serializzazione di grafi di oggetti: cosa succede «sotto il cofano»?
Quando serializzi un oggetto, Java «cammina» su tutti i suoi campi (e sui campi dei campi, e così via), serializzando ogni oggetto una sola volta. Se un oggetto viene incontrato di nuovo (ad esempio in un riferimento ciclico), Java scrive un riferimento speciale invece di serializzarlo di nuovo.
Visualizzazione (diagramma di flusso)
graph TD
A[Oggetto A] -- campo --> B[Oggetto B]
B -- campo --> C[Oggetto C]
C -- campo --> A
Java prima serializza A, poi B, quindi C, e quando incontra di nuovo A scrive «un riferimento all’oggetto A già serializzato». In deserializzazione la struttura viene ripristinata mantenendo tutti i collegamenti.
6. Peculiarità della serializzazione dei grafi
- I cicli non fanno paura: la serializzazione standard di Java supporta i riferimenti ciclici, non va in loop e non provoca StackOverflow.
- Tutti gli oggetti devono essere serializzabili: se anche solo un oggetto nel grafo non implementa Serializable, la serializzazione fallirà su quell’oggetto.
- Gli stessi oggetti non vengono duplicati: se lo stesso oggetto compare in più punti del grafo, dopo la deserializzazione sarà lo stesso oggetto (per riferimento).
- I tipi degli oggetti vengono preservati: anche se la collezione è dichiarata come List<Animal>, dopo la deserializzazione otterrai oggetti dei loro classi reali (Cat, Dog, ecc.).
7. Errori tipici nella serializzazione di oggetti annidati e gerarchici
Errore n. 1: non tutte le classi sono serializzabili.
Molto spesso si dimentica di aggiungere implements Serializable a una delle proprie classi che si trova dentro una collezione o un oggetto annidato. Risultato: NotSerializableException e delusione. Controlla la catena di annidamento!
Errore n. 2: perdita di riferimenti con serializzazione manuale.
Se implementi i metodi writeObject/readObject manualmente e dimentichi di serializzare uno dei campi (ad esempio un riferimento al genitore o a una collezione annidata), dopo la deserializzazione la struttura risulterà danneggiata. Verifica sempre il ripristino.
Errore n. 3: utilizzo di transient per campi necessari.
Se contrassegni un campo necessario come transient, esso non finirà nel flusso serializzato e, dopo il ripristino, sarà null o avrà il valore predefinito. Questo può compromettere l’integrità del grafo di oggetti.
Errore n. 4: modifica della struttura delle classi tra serializzazione e deserializzazione.
Se hai modificato la struttura di una classe (ad esempio aggiungendo un campo) dopo aver serializzato un oggetto, al momento della deserializzazione sono possibili errori o perdita di dati. Usa serialVersionUID e mantieni la compatibilità.
Errore n. 5: serializzazione di grafi di grandi dimensioni.
Strutture complesse e molto interconnesse possono portare a file molto grandi e a tempi lunghi di serializzazione/deserializzazione. Tieni d’occhio le dimensioni e, se possibile, suddividi in parti.
Errore n. 6: serializzazione di collezioni «grezze».
Se dichiari una collezione senza parametro generico (ad esempio semplicemente List), dopo la deserializzazione dovrai effettuare cast espliciti dei tipi, con il rischio di ClassCastException. Usa i generics e verifica i tipi.
GO TO FULL VERSION