CodeGym /Corsi /JAVA 25 SELF /Il problema dei riferimenti ciclici: rilevamento e gestio...

Il problema dei riferimenti ciclici: rilevamento e gestione

JAVA 25 SELF
Livello 44, Lezione 2
Disponibile

1. Che cosa sono i riferimenti ciclici?

Un riferimento ciclico è una situazione in cui un oggetto (o una collezione) contiene direttamente o indirettamente un riferimento a se stesso. Nelle collezioni questo capita più spesso di quanto sembri, soprattutto se costruisci strutture dati complesse o lavori con grafi.

Esempi dal mondo reale

  • Due oggetti si riferiscono l’uno all’altro:
    Per esempio, hai una classe User che ha un riferimento a Profile, e Profile ha un riferimento di ritorno a User.
  • Una collezione contiene se stessa:
    L’esempio più semplice e «divertente»:
List<Object> list = new ArrayList<>();
list.add(list); // Ops! list contiene se stessa
  • Grafo di oggetti:
    Oggetti interconnessi, ad esempio nodi di un albero, in cui ciascuno può avere un riferimento al genitore e ai figli.

Visualizzazione

graph LR
A[User] -- profile --> B[Profile]
B -- user --> A

Oppure per una collezione:

graph TD
L[List] -- add(self) --> L

Perché può essere un problema?

Se il serializzatore non è in grado di tracciare i cicli, può «andare all’infinito», tentando di serializzare gli oggetti annidati ancora e ancora, finché non esaurisce lo stack (StackOverflowError). La buona notizia: la serializzazione standard di Java conosce questi trucchi e sa come gestirli!

2. Come funziona la serializzazione standard di Java con i cicli?

Quando serializzi un oggetto tramite ObjectOutputStream, Java traccia automaticamente quali oggetti sono già stati serializzati in quel flusso. Se il serializzatore incontra di nuovo lo stesso oggetto, non lo serializza di nuovo, ma scrive un riferimento speciale a quello già serializzato. Questo permette di serializzare correttamente anche strutture molto complesse con cicli.

Esempio: una collezione che contiene se stessa

Proviamo a serializzare una collezione che contiene se stessa. Non è uno scherzo — questo codice si compila e funziona davvero:

import java.io.*;
import java.util.*;

public class CyclicListDemo {
    public static void main(String[] args) throws Exception {
        List<Object> list = new ArrayList<>();
        list.add("Hello, cyclic world!");
        list.add(list); // Aggiungiamo se stessa

        // Serializzazione
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cyclic_list.ser"))) {
            out.writeObject(list);
        }

        // Deserializzazione
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cyclic_list.ser"))) {
            List<?> deserialized = (List<?>) in.readObject();

            System.out.println(deserialized.get(0)); // "Hello, cyclic world!"
            System.out.println(deserialized.get(1) == deserialized); // true!
        }
    }
}

Risultato:
— Il primo elemento è una stringa normale.
— Il secondo elemento è... la collezione stessa! Il controllo deserialized.get(1) == deserialized restituirà true.
Java non è entrata in un loop e non è andata in errore, ma ha ricostruito correttamente la struttura dei riferimenti.

Come funziona internamente?

ObjectOutputStream mantiene un «registro» interno degli oggetti serializzati. Se un oggetto è già stato serializzato, nel flusso viene scritto un riferimento speciale (handle) a esso, e non il suo contenuto. In fase di deserializzazione ObjectInputStream ripristina le stesse relazioni.

3. Problemi e limitazioni

  • Hai serializzato per errore un grafo enorme.
    Se la tua struttura dati è molto grande e contiene molti riferimenti incrociati, la serializzazione può richiedere molto tempo e produrre un file enorme.
  • Modifica della struttura delle classi.
    Se hai serializzato un oggetto e poi ne hai modificato la classe (per esempio, aggiunto o rimosso un campo), in fase di deserializzazione può verificarsi InvalidClassException. Soprattutto se cambiano i campi che partecipano al ciclo.
  • Problemi con la serializzazione personalizzata.
    Se implementi manualmente i metodi writeObject e readObject, devi gestire correttamente i cicli da solo. Se dimentichi di chiamare i metodi predefiniti (defaultWriteObject/defaultReadObject), il serializzatore non riuscirà a tracciare i cicli.
  • Serializzazione in altri formati (ad esempio, JSON).
    La serializzazione standard di Java (ObjectOutputStream) gestisce i cicli, ma se serializzi oggetti in JSON (ad esempio tramite Jackson o Gson), i cicli possono portare a StackOverflowError o a eccezioni. Queste librerie, per impostazione predefinita, non sanno lavorare con i cicli — servono impostazioni esplicite.

4. Gestione dei riferimenti ciclici

Nella serializzazione standard di Java

Tutto funziona «pronto all’uso»! Non devi fare nulla di speciale — Java rileverà i cicli e conserverà la struttura dei riferimenti.

Manuale: serializzazione in altri formati

  • Usare identificatori al posto dei riferimenti.
    Invece di conservare riferimenti ad altri oggetti, conserva i loro identificatori univoci. Dopo la deserializzazione ricostruisci i collegamenti tramite questi id.
  • Annotazioni o impostazioni speciali.
    In Jackson puoi usare le annotazioni @JsonIdentityInfo oppure la coppia @JsonBackReference/@JsonManagedReference per controllare la serializzazione dei cicli.
  • Rimuovere i cicli prima della serializzazione.
    Azzerare temporaneamente i campi che creano il ciclo, escluderli con transient o tramite annotazioni.

Esempio: serializzazione di un grafo con cicli

Vediamo un esempio con una struttura più complessa — un grafo di utenti in cui ogni utente può essere amico di un altro utente.

import java.io.*;
import java.util.*;

class User implements Serializable {
    String name;
    List<User> friends = new ArrayList<>();

    User(String name) { this.name = name; }

    public String toString() {
        return name + " (" + friends.size() + " friends)";
    }
}

public class CyclicGraphDemo {
    public static void main(String[] args) throws Exception {
        User alice = new User("Alice");
        User bob = new User("Bob");
        User charlie = new User("Charlie");

        // Creiamo amicizie con cicli
        alice.friends.add(bob);
        bob.friends.add(charlie);
        charlie.friends.add(alice); // ciclo!

        // Serializzazione
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("users.ser"))) {
            out.writeObject(alice);
        }

        // Deserializzazione
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("users.ser"))) {
            User restoredAlice = (User) in.readObject();
            System.out.println(restoredAlice);
            System.out.println(restoredAlice.friends.get(0));
            System.out.println(restoredAlice.friends.get(0).friends.get(0));
            System.out.println(restoredAlice.friends.get(0).friends.get(0).friends.get(0) == restoredAlice); // true!
        }
    }
}

Risultato:
— La struttura con il ciclo viene ripristinata: dopo tre passaggi tra gli amici si torna ad Alice.
— Java non si è confusa e non è entrata in un loop.

5. Errori tipici nella gestione dei riferimenti ciclici

Errore n. 1: serializzazione in JSON senza supporto dei cicli. Se decidi di serializzare un oggetto con cicli tramite Jackson o Gson senza configurazione, molto probabilmente otterrai StackOverflowError. Ad esempio, se hai una classe Node in cui ogni nodo fa riferimento al genitore e ai figli, la serializzazione di tale albero in JSON porterà a un annidamento infinito.

Errore n. 2: violazione della struttura delle classi. Se dopo la serializzazione modifichi la struttura della classe (per esempio, aggiungi un campo), durante la deserializzazione del vecchio file può verificarsi un errore di incompatibilità. È particolarmente critico per grafi complessi con cicli.

Errore n. 3: serializzazione artigianale senza considerare i cicli. Se implementi manualmente writeObject/readObject e non chiami defaultWriteObject, Java non riuscirà a tracciare i cicli e la serializzazione o entrerà in loop, oppure la struttura dei riferimenti si romperà in deserializzazione.

Errore n. 4: inserimento accidentale della collezione in se stessa. A volte gli sviluppatori inesperti aggiungono accidentalmente una collezione a se stessa (per esempio durante la copia degli elementi), senza rendersi conto di aver creato un ciclo. Di conseguenza, la serializzazione funzionerà, ma la logica del programma può diventare strana e imprevedibile.

1
Compito
JAVA 25 SELF, livello 44, lezione 2
Bloccato
Lista paradossale: una collezione che contiene se stessa 🤯
Lista paradossale: una collezione che contiene se stessa 🤯
1
Compito
JAVA 25 SELF, livello 44, lezione 2
Bloccato
Salvataggio della topologia di rete: serializzazione di un grafo con percorso ciclico 🌐
Salvataggio della topologia di rete: serializzazione di un grafo con percorso ciclico 🌐
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION