CodeGym /Corsi /JAVA 25 SELF /Identità degli oggetti nella serializzazione binaria

Identità degli oggetti nella serializzazione binaria

JAVA 25 SELF
Livello 44, Lezione 3
Disponibile

1. Introduzione

In Java (e in generale nella OOP) ogni oggetto ha un’identità — non è solo l’insieme dei valori dei suoi campi, ma proprio «chi è» in memoria. Due oggetti possono essere «equivalenti» (per esempio, a.equals(b) restituisce true), ma essere diversi per identità (a != b). L’identità è quando due variabili puntano allo stesso oggetto in memoria (a == b).

Perché è importante per la serializzazione?

Quando si serializza (si salva) un grafo di oggetti (per esempio, un albero, una lista o semplicemente due oggetti che fanno riferimento allo stesso oggetto annidato), è importante conservare non solo i valori dei campi, ma anche la struttura dei riferimenti tra gli oggetti.

Se nel vostro grafo ci sono riferimenti ripetuti (più oggetti fanno riferimento allo stesso oggetto annidato) o persino riferimenti ciclici (A → B → A), dopo la deserializzazione questi legami devono rimanere gli stessi.

Esempio: avete due oggetti A e B, entrambi fanno riferimento allo stesso oggetto C. Dopo serializzazione e deserializzazione deve valere: a.c == b.c (lo stesso oggetto in memoria). Se la serializzazione «copiasse» semplicemente gli oggetti, dopo il ripristino si otterrebbero due oggetti C diversi — e sarebbe un grafo differente.

2. Come ObjectOutputStream risolve il problema dell’identità

In Java per la serializzazione binaria si usa una coppia di classi: ObjectOutputStream (per la scrittura) e ObjectInputStream (per la lettura).

ObjectOutputStream non è semplicemente «scrivere un oggetto su file». È intelligente:

  • Tiene traccia di tutti gli oggetti già serializzati nell’ambito dello stesso flusso di scrittura (una sessione).
  • Se compare un riferimento a un oggetto già serializzato, non lo scrive di nuovo, ma registra un apposito «riferimento ripetuto» (reference handle).
  • In deserializzazione ObjectInputStream ripristina la struttura in modo che i riferimenti ripetuti puntino allo stesso oggetto in memoria.

Questo funziona anche per i riferimenti ciclici! Cioè, se avete A → B → A, serializzazione e deserializzazione non andranno in loop e non falliranno: i riferimenti verranno ripristinati correttamente.

Come è implementato?

All’interno di ObjectOutputStream viene usata una tabella speciale (identity map) in cui sono memorizzati gli oggetti già serializzati. Quando incontra un nuovo oggetto, lo aggiunge alla tabella e lo serializza completamente. Quando incontra un oggetto presente nella tabella — nel flusso viene scritto soltanto un «riferimento ripetuto» (handle).

3. Dimostrazione con esempi

Esempio 1: Grafo con riferimenti ciclici (A → B, B → A)

import java.io.*;

class Node implements Serializable {
    String name;
    Node next;

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

public class CyclicSerializationDemo {
    public static void main(String[] args) throws Exception {
        Node a = new Node("A");
        Node b = new Node("B");
        a.next = b;
        b.next = a; // Ciclo!

        // Serializzazione
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cyclic.dat"))) {
            out.writeObject(a);
        }

        // Deserializzazione
        Node a2;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cyclic.dat"))) {
            a2 = (Node) in.readObject();
        }

        System.out.println("a2.name = " + a2.name); // A
        System.out.println("a2.next.name = " + a2.next.name); // B
        System.out.println("a2.next.next == a2: " + (a2.next.next == a2)); // true!
    }
}

Output:

a2.name = A
a2.next.name = B
a2.next.next == a2: true

Commento:
Il ciclo è stato preservato! Dopo la deserializzazione a2.next.next punta di nuovo a a2.

Esempio 2: Riferimenti ripetuti (A → C, B → C)

import java.io.*;

class Wrapper implements Serializable {
    String name;
    Object ref;

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

public class SharedReferenceDemo {
    public static void main(String[] args) throws Exception {
        Wrapper a = new Wrapper("A");
        Wrapper b = new Wrapper("B");
        Wrapper c = new Wrapper("C");

        a.ref = c;
        b.ref = c;

        // Serializzazione
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("shared.dat"))) {
            out.writeObject(new Wrapper[] {a, b});
        }

        // Deserializzazione
        Wrapper[] arr;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("shared.dat"))) {
            arr = (Wrapper[]) in.readObject();
        }

        Wrapper a2 = arr[0];
        Wrapper b2 = arr[1];

        System.out.println("a2.ref == b2.ref: " + (a2.ref == b2.ref)); // true!
        System.out.println("a2.ref.name = " + ((Wrapper)a2.ref).name); // C
    }
}

Output:

a2.ref == b2.ref: true
a2.ref.name = C

Commento:
Dopo la deserializzazione entrambi i riferimenti puntano di nuovo allo stesso oggetto.

4. Relazione con writeReplace e readResolve

In Java ci si può «intromettere» nel processo di (de)serializzazione tramite metodi speciali:

writeReplace() — viene chiamato prima della serializzazione dell’oggetto. Si può restituire un altro oggetto che verrà serializzato al posto dell’originale.
readResolve() — viene chiamato dopo la deserializzazione dell’oggetto. Si può restituire un altro oggetto che verrà usato al posto di quello appena creato.

Impatto sull’identità:

  • Se si usa writeReplace() o readResolve(), l’oggetto restituito da questi metodi è quello che finirà nella tabella dei riferimenti.
  • Questo può cambiare l’identità: se readResolve() restituisce un oggetto nuovo/diverso, i riferimenti ripetuti potrebbero puntare a istanze diverse.
  • Fate attenzione: potete sia «rompere» l’identità sia garantirla intenzionalmente (l’esempio classico è il Singleton tramite readResolve(), dove tutti i riferimenti dopo la deserializzazione puntano alla stessa istanza singleton).

5. Pratica: codice che mostra il mantenimento dell’identità

import java.io.*;

class Shared implements Serializable {
    String value;
    Shared(String value) { this.value = value; }
}

class Holder implements Serializable {
    String name;
    Shared shared;
    Holder(String name, Shared shared) {
        this.name = name;
        this.shared = shared;
    }
}

public class IdentityDemo {
    public static void main(String[] args) throws Exception {
        Shared c = new Shared("C");
        Holder a = new Holder("A", c);
        Holder b = new Holder("B", c);

        // Serializzazione
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("identity.dat"))) {
            out.writeObject(new Holder[] {a, b});
        }

        // Deserializzazione
        Holder[] arr;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("identity.dat"))) {
            arr = (Holder[]) in.readObject();
        }

        Holder a2 = arr[0];
        Holder b2 = arr[1];

        System.out.println("a2.shared == b2.shared: " + (a2.shared == b2.shared)); // true!
        System.out.println("a2.shared.value = " + a2.shared.value); // C
    }
}

Output:

a2.shared == b2.shared: true
a2.shared.value = C

Commento:
L’identità dell’oggetto c è stata preservata: dopo la deserializzazione entrambi i riferimenti puntano alla stessa istanza.

6. Conclusioni ed errori tipici

Errore n. 1: serializzare separatamente oggetti con riferimenti ripetuti. Se scrivete ogni oggetto con la propria istanza di ObjectOutputStream, i riferimenti tra di loro non verranno conservati — le istanze deserializzate saranno diverse, anche se in origine si trattava dello stesso riferimento.

Errore n. 2: uso scorretto di writeReplace() e readResolve(). Questi metodi possono sostituire l’oggetto durante la (de)serializzazione, modificandone l’identità. Senza comprendere la meccanica, si possono ottenere istanze inattese in uscita.

Errore n. 3: effetti imprevisti da riferimenti condivisi mutabili. Se più oggetti fanno riferimento a un unico oggetto annidato mutabile (per esempio, una lista), dopo la deserializzazione rimarrà così. Una modifica in un punto influenzerà tutti gli altri.

Errore n. 4: aspettarsi «nuovi» oggetti dopo la serializzazione. La serializzazione non crea una struttura «fresca» — ripristina i riferimenti originari, come nell’originale. Questo può sorprendere quando si lavora con cache o oggetti modello.

1
Compito
JAVA 25 SELF, livello 44, lezione 3
Bloccato
Collaborazione su un documento condiviso: preservare oggetti condivisi 📝
Collaborazione su un documento condiviso: preservare oggetti condivisi 📝
1
Compito
JAVA 25 SELF, livello 44, lezione 3
Bloccato
Gestione dell'inventario: identità degli oggetti durante l'archiviazione separata 📦
Gestione dell'inventario: identità degli oggetti durante l'archiviazione separata 📦
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION