CodeGym /Corsi /JAVA 25 SELF /Oggetti annidati e gerarchici: serializzazione di grafi

Oggetti annidati e gerarchici: serializzazione di grafi

JAVA 25 SELF
Livello 44 , Lezione 1
Disponibile

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.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION