CodeGym /Corsi /JAVA 25 SELF /Configurazione del comportamento della serializzazione: m...

Configurazione del comportamento della serializzazione: metodi personalizzati

JAVA 25 SELF
Livello 43 , Lezione 3
Disponibile

1. Metodi writeReplace e readResolve: teoria

A volte i meccanismi standard di serializzazione non sono sufficienti. Immaginate una situazione: avete un singleton (una classe di cui può esistere una sola istanza in tutta l’applicazione) e volete che, dopo la deserializzazione, rimanga l’unica istanza (e non ne appaia un nuovo clone). Oppure volete serializzare non l’oggetto stesso, ma una sua versione «leggera» (proxy), per nascondere dettagli di implementazione o risparmiare spazio.

In Java esistono metodi speciali per questo: writeReplace e readResolve. Il loro compito è sostituire l’oggetto in serializzazione o deserializzazione con un altro.

Un’analogia semplice:
È come se steste spedendo un pacco a un amico, ma invece di voi nella scatola mettete un vostro doppio giocattolo. E quando l’amico apre il pacco, al posto del giocattolo si ritrova voi — quelli veri! (Nella vita reale non funziona così, ma in Java sì.)

writeReplace

Il metodo private Object writeReplace() viene chiamato sull’oggetto prima della serializzazione. Può restituire qualsiasi oggetto, che verrà effettivamente serializzato al posto dell’originale. Se non è implementato — viene serializzato l’oggetto stesso.

Firma:


private Object writeReplace() throws ObjectStreamException

readResolve

Il metodo private Object readResolve() viene chiamato sull’oggetto dopo la deserializzazione. Consente di sostituire l’oggetto appena creato con un altro (per esempio, restituire un singleton o un’istanza in cache).

Firma:

private Object readResolve() throws ObjectStreamException

Importante:
Entrambi i metodi devono essere private e restituire Object. È un requisito della specifica di serializzazione di Java. Se li rendete public, la serializzazione li ignorerà semplicemente.

2. Uso di writeReplace e readResolve nella pratica

Singleton e readResolve

Un singleton è semplicemente una classe di cui può esistere una sola istanza in tutta l’applicazione. Se un tale oggetto viene serializzato e poi ricostruito, senza il metodo readResolve apparirà una nuova istanza e la regola dell’«unicità» verrà violata. Con readResolve si può invece restituire proprio quell’oggetto, preservando l’idea del singleton.

import java.io.*;

public class MySingleton implements Serializable {
    private static final MySingleton INSTANCE = new MySingleton();
    private MySingleton() {}

    public static MySingleton getInstance() {
        return INSTANCE;
    }

    // Garantiamo che dopo la deserializzazione venga restituito proprio INSTANCE
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

Spiegazione:
Senza readResolve, dopo la deserializzazione apparirà un nuovo oggetto, non uguale (rispetto a ==) al singleton originale. Con readResolve — viene sempre restituito INSTANCE.

Verifichiamo nella pratica:

MySingleton s1 = MySingleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.bin"));
out.writeObject(s1);
out.close();

ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.bin"));
MySingleton s2 = (MySingleton) in.readObject();
in.close();

System.out.println(s1 == s2); // true, se esiste readResolve; false — senza di esso

writeReplace: serializzazione di un oggetto proxy

A volte un oggetto è troppo pesante da serializzare, contiene dati sensibili o semplicemente non deve uscire all’esterno nella sua forma completa. In questi casi si può serializzare un «sostituto» — un oggetto proxy.

Esempio:
Supponiamo di avere la classe User con una password privata. Non vogliamo che la password venga serializzata.

import java.io.*;

public class User implements Serializable {
    private String username;
    private transient String password; // transient — non viene serializzato

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // Invece di User serializziamo solo UserProxy
    private Object writeReplace() throws ObjectStreamException {
        return new UserProxy(username);
    }

    // Classe proxy — solo per la serializzazione
    private static class UserProxy implements Serializable {
        private String username;
        public UserProxy(String username) {
            this.username = username;
        }

        private Object readResolve() throws ObjectStreamException {
            // Nella vita reale la password non si può ripristinare — restituiamo User con password vuota
            return new User(username, "");
        }
    }
}

Spiegazione:

  • In serializzazione User si trasforma in UserProxy (senza password).
  • In deserializzazione UserProxy torna a essere User (ma la password è vuota).

3. Personalizzazione della serializzazione per oggetti immutabili

Gli oggetti immutabili usano spesso campi final privati e non hanno setter. Con la serializzazione standard Java può aggirare questa limitazione, ma talvolta è meglio controllare esplicitamente il processo tramite writeReplace/readResolve.

Esempio: Value Object

import java.io.*;

public final class Money implements Serializable {
    private final int amount;
    private final String currency;

    public Money(int amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    private Object writeReplace() throws ObjectStreamException {
        return new MoneyProxy(amount, currency);
    }

    private static class MoneyProxy implements Serializable {
        private final int amount;
        private final String currency;

        MoneyProxy(int amount, String currency) {
            this.amount = amount;
            this.currency = currency;
        }

        private Object readResolve() throws ObjectStreamException {
            return new Money(amount, currency);
        }
    }
}

Spiegazione:

  • In serializzazione Money si trasforma in MoneyProxy (POJO).
  • In deserializzazione MoneyProxy torna a essere Money.

Interazione con writeObject/readObject

I metodi writeReplace/readResolve funzionano in modo indipendente da writeObject/readObject. Se sono definiti entrambi i meccanismi, prima viene chiamato writeReplace e, sull’oggetto restituito, writeObject (se implementa Serializable).

Schema:

flowchart LR
    A[Oggetto] -- writeReplace --> B[Oggetto proxy]
    B -- writeObject --> C[Flusso di byte]
    C -- readObject --> D[Oggetto proxy]
    D -- readResolve --> E[Oggetto finale]

4. Pratica: serializzazione con sostituzione dell’oggetto

Aggiungiamo una serializzazione personalizzata alla vostra applicazione didattica — per esempio, per la classe Person, in modo che in serializzazione venga scritto solo il nome e l’età venga ignorata (supponiamo per motivi di privacy).

Passo 1. Classe principale

import java.io.*;

public class Person implements Serializable {
    private String name;
    private int age; // non vogliamo serializzare

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private Object writeReplace() throws ObjectStreamException {
        return new PersonProxy(name);
    }

    private static class PersonProxy implements Serializable {
        private final String name;

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

        private Object readResolve() throws ObjectStreamException {
            return new Person(name, -1); // -1 — "età sconosciuta"
        }
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

Passo 2. Test

public class TestCustomSerialization {
    public static void main(String[] args) throws Exception {
        Person original = new Person("Alice", 30);

        // Serializzazione
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"));
        out.writeObject(original);
        out.close();

        // Deserializzazione
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"));
        Person deserialized = (Person) in.readObject();
        in.close();

        System.out.println("Prima della serializzazione: " + original);
        System.out.println("Dopo la deserializzazione: " + deserialized);
    }
}

Risultato:

Prima della serializzazione: Person{name='Alice', age=30}
Dopo la deserializzazione: Person{name='Alice', age=-1}

Come si vede, l’età non è stata serializzata — tutto secondo i piani!

5. Caratteristiche e sfumature

Quando usare writeReplace/readResolve?

  • Quando è necessario serializzare solo una parte dello stato dell’oggetto.
  • Per serializzare/deserializzare oggetti proxy.
  • Per supportare il pattern Singleton.
  • Per oggetti immutabili o complessi, la cui struttura interna può cambiare.

Quando non conviene usarli?

  • Se è possibile cavarsela con campi transient o con writeObject/readObject.
  • Se l’oggetto non deve essere sostituito con un altro.

Compatibilità con l’ereditarietà

Se una superclasse definisce writeReplace/readResolve, questi verranno chiamati anche per le sottoclassi (se non sono sovrascritti). Fate attenzione alle gerarchie!

6. Errori tipici nella serializzazione personalizzata

Errore n. 1: Visibilità dei metodi errata. Se si rendono writeReplace/readResolve non private, la serializzazione non li chiamerà. Solo private!

Errore n. 2: Disallineamento dei tipi restituiti. writeReplace/readResolve devono restituire Object. Anche se in pratica restituite il vostro tipo, dichiarate il metodo con tipo di ritorno Object.

Errore n. 3: Perdita di dati. Se l’oggetto proxy non contiene tutti i dati necessari per ripristinare l’oggetto originale, parte dell’informazione andrà persa. Verificate sempre di poter ricostruire l’oggetto.

Errore n. 4: Violazione degli invarianti. readResolve deve restituire un oggetto che soddisfi le aspettative del programma (per esempio, per un singleton — proprio INSTANCE).

Errore n. 5: Eccezioni non gestite. writeReplace/readResolve possono lanciare ObjectStreamException. Gestitela oppure propagatela esplicitamente.

1
Compito
JAVA 25 SELF, livello 43, lezione 3
Bloccato
Nexus Cosmico: Garantire l'Unicità dell'Esistenza
Nexus Cosmico: Garantire l'Unicità dell'Esistenza
1
Compito
JAVA 25 SELF, livello 43, lezione 3
Bloccato
Stoccaggio dei Dati Segreti: Mascheramento delle Informazioni durante la Trasmissione
Stoccaggio dei Dati Segreti: Mascheramento delle Informazioni durante la Trasmissione
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION