JavaRush /Java Blog /Random-IT /Serializzazione così com'è. Parte 1
articles
Livello 15

Serializzazione così com'è. Parte 1

Pubblicato nel gruppo Random-IT
A prima vista, la serializzazione sembra un processo banale. Davvero, cosa potrebbe essere più semplice? Dichiarata la classe per implementare l'interfaccia java.io.Serializablee il gioco è fatto. Puoi serializzare la classe senza problemi. Serializzazione così com'è.  Parte 1 - 1In teoria, questo è vero. In pratica, ci sono molte sottigliezze. Sono legati alle prestazioni, alla deserializzazione, alla sicurezza della classe. E con molti altri aspetti. Tali sottigliezze saranno discusse. Questo articolo può essere suddiviso nelle seguenti parti:
  • Sottigliezze dei meccanismi
  • Perché è necessario?Externalizable
  • Prestazione
  • ma d'altra parte
  • La sicurezza dei dati
  • Serializzazione degli oggettiSingleton
Passiamo alla prima parte -

Sottigliezze dei meccanismi

Prima di tutto, una domanda veloce. Quanti modi esistono per rendere serializzabile un oggetto? La pratica dimostra che oltre il 90% degli sviluppatori risponde a questa domanda più o meno allo stesso modo (fino alla formulazione): esiste solo un modo. Nel frattempo ce ne sono due. Non tutti ricordano il secondo, tanto meno dicono qualcosa di comprensibile sulle sue caratteristiche. Allora quali sono questi metodi? Tutti ricordano il primo. Questa è l'implementazione già menzionata java.io.Serializablee non richiede alcuno sforzo. Anche il secondo metodo prevede l'implementazione di un'interfaccia, ma diversa: java.io.Externalizable. A differenza di java.io.Serializable, contiene due metodi che devono essere implementati: writeExternal(ObjectOutput)e readExternal(ObjectInput). Questi metodi contengono la logica di serializzazione/deserializzazione. Commento.SerializableIn quanto segue , a volte farò riferimento alla serializzazione con implementazione come standard e implementazione Externalizablecome estesa. Un altrocommento. Non toccherò deliberatamente ora le opzioni di controllo della serializzazione standard come la definizione readObjecte writeObject, perché Penso che questi metodi siano in qualche modo errati. Questi metodi non sono definiti nell'interfaccia Serializablee sono, di fatto, strumenti per aggirare le limitazioni e rendere flessibile la serializzazione standard. ExternalizableI metodi che forniscono flessibilità sono integrati fin dall'inizio . Facciamo un'altra domanda. Come funziona effettivamente la serializzazione standard, utilizzando java.io.Serializable? E funziona tramite l'API Reflection. Quelli. la classe viene analizzata come un insieme di campi, ognuno dei quali viene scritto nel flusso di output. Penso che sia chiaro che questa operazione non è ottimale in termini di prestazioni. Quanto esattamente lo scopriremo più tardi. Esiste un'altra importante differenza tra i due metodi di serializzazione menzionati. Vale a dire, nel meccanismo di deserializzazione. Se utilizzata, Serializablela deserializzazione avviene in questo modo: la memoria viene allocata per un oggetto, dopodiché i suoi campi vengono riempiti con i valori dello stream. Il costruttore dell'oggetto non viene chiamato. Qui dobbiamo considerare questa situazione separatamente. Ok, la nostra classe è serializzabile. E il suo genitore? Completamente facoltativo! Inoltre, se erediti una classe da Object, il genitore NON è sicuramente serializzabile. E anche se Objectnon sappiamo nulla dei campi, potrebbero benissimo esistere nelle nostre classi madri. Cosa accadrà loro? Non entreranno nel flusso di serializzazione. Che valori assumeranno con la deserializzazione? Diamo un'occhiata a questo esempio:
package ru.skipy.tests.io;

import java.io.*;

/**
 * ParentDeserializationTest
 *
 * @author Eugene Matyushkin aka Skipy
 * @since 05.08.2010
 */
public class ParentDeserializationTest {

    public static void main(String[] args){
        try {
            System.out.println("Creating...");
            Child c = new Child(1);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            c.field = 10;
            System.out.println("Serializing...");
            oos.writeObject(c);
            oos.flush();
            baos.flush();
            oos.close();
            baos.close();
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            System.out.println("Deserializing...");
            Child c1 = (Child)ois.readObject();
            System.out.println("c1.i="+c1.getI());
            System.out.println("c1.field="+c1.getField());
        } catch (IOException ex){
            ex.printStackTrace();
        } catch (ClassNotFoundException ex){
            ex.printStackTrace();
        }
    }

    public static class Parent {
        protected int field;
        protected Parent(){
            field = 5;
            System.out.println("Parent::Constructor");
        }
        public int getField() {
            return field;
        }
    }

    public static class Child extends Parent implements Serializable{
        protected int i;
        public Child(int i){
            this.i = i;
            System.out.println("Child::Constructor");
        }
        public int getI() {
            return i;
        }
    }
}
È trasparente: abbiamo una classe genitore non serializzabile e una classe figlia serializzabile. E questo è ciò che accade:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
Cioè, durante la deserializzazione, viene chiamato il costruttore senza parametri della classe genitore NON serializzabile . E se non esiste un costruttore di questo tipo, si verificherà un errore durante la deserializzazione. Il costruttore dell'oggetto figlio, quello che stiamo deserializzando, non viene chiamato, come detto sopra. Ecco come si comportano i meccanismi standard quando vengono utilizzati Serializable. Quando lo si utilizza, Externalizablela situazione è diversa. Innanzitutto, viene chiamato il costruttore senza parametri, quindi viene chiamato il metodo readExternal sull'oggetto creato, che in realtà legge tutti i suoi dati. Pertanto, qualsiasi classe che implementa l'interfaccia Externalizable deve avere un costruttore pubblico senza parametri! Inoltre, poiché verranno considerati anche tutti i discendenti di tale classe per implementare l'interfaccia Externalizable, anch'essi dovranno avere un costruttore senza parametri! Andiamo oltre. Esiste un modificatore di campo come transient. Ciò significa che questo campo non deve essere serializzato. Tuttavia, come tu stesso capisci, questa istruzione influisce solo sul meccanismo di serializzazione standard. Quando viene utilizzato, Externalizablenessuno si preoccupa di serializzare questo campo e nemmeno di sottrarlo. Se un campo viene dichiarato transitorio, quando l'oggetto viene deserializzato assume il valore predefinito. Un altro punto piuttosto sottile. Con la serializzazione standard, i campi che presentano il modificatore staticnon vengono serializzati. Di conseguenza, dopo la deserializzazione questo campo non cambia il suo valore. Naturalmente, durante l'implementazione, Externalizablenessuno si preoccupa di serializzare e deserializzare questo campo, ma consiglio vivamente di non farlo, perché questo può portare a sottili errori. I campi con un modificatore finalvengono serializzati come campi normali. Con un'eccezione: non possono essere deserializzati quando si utilizza Externalizable. Perché final-поляdevono essere inizializzati nel costruttore, dopodiché sarà impossibile modificare il valore di questo campo in readExternal. Di conseguenza, se è necessario serializzare un oggetto che dispone finaldi un campo, sarà necessario utilizzare solo la serializzazione standard. Un altro punto che molte persone non sanno. La serializzazione standard tiene conto dell'ordine in cui i campi vengono dichiarati in una classe. In ogni caso, questo era il caso nelle versioni precedenti; nella versione JVM 1.6 dell'implementazione Oracle, l'ordine non è più importante, lo sono il tipo e il nome del campo. È molto probabile che la composizione dei metodi influenzi il meccanismo standard, nonostante il fatto che i campi possano generalmente rimanere gli stessi. Per evitare ciò, esiste il seguente meccanismo. Ad ogni classe che implementa l'interfaccia Serializable, viene aggiunto un ulteriore campo in fase di compilazione:private static final long serialVersionUID. Questo campo contiene l'identificatore di versione univoco della classe serializzata. Viene calcolato in base al contenuto della classe: campi, ordine di dichiarazione, metodi, ordine di dichiarazione. Di conseguenza, con qualsiasi modifica nella classe, questo campo cambierà il suo valore. Questo campo viene scritto nel flusso quando la classe viene serializzata. A proposito, questo è forse l'unico caso a me noto quando staticun campo viene serializzato. Durante la deserializzazione, il valore di questo campo viene confrontato con quello della classe nella macchina virtuale. Se i valori non corrispondono, viene lanciata un'eccezione come questa:
java.io.InvalidClassException: test.ser2.ChildExt;
    local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
                                   local class serialVersionUID = 1465687698753363969
Esiste, tuttavia, un modo per, se non aggirare, ingannare questo controllo. Ciò può essere utile se l'insieme dei campi della classe e il loro ordine sono già definiti, ma i metodi della classe potrebbero cambiare. In questo caso la serializzazione non è a rischio, ma il meccanismo standard non consentirà la deserializzazione dei dati utilizzando il bytecode della classe modificata. Ma, come ho detto, può essere ingannato. Vale a dire, definire manualmente il campo nella classe private static final long serialVersionUID. In linea di principio, il valore di questo campo può essere assolutamente qualsiasi. Alcune persone preferiscono impostarlo uguale alla data in cui il codice è stato modificato. Alcuni addirittura usano 1 litro. Per ottenere il valore standard (quello calcolato internamente), è possibile utilizzare l'utilità serialver inclusa nell'SDK. Una volta definito in questo modo, il valore del campo sarà fisso, quindi la deserializzazione sarà sempre consentita. Inoltre, nella versione 5.0, nella documentazione appariva approssimativamente quanto segue: è altamente raccomandato che tutte le classi serializzabili dichiarino esplicitamente questo campo, poiché il calcolo predefinito è molto sensibile ai dettagli della struttura della classe, che può variare a seconda dell'implementazione del compilatore, e quindi causare InvalidClassExceptionconseguenze inaspettate. È meglio dichiarare questo campo come private, perché si riferisce esclusivamente alla classe in cui è dichiarato. Sebbene il modificatore non sia specificato nelle specifiche. Consideriamo ora questo aspetto. Diciamo che abbiamo questa struttura di classi:
public class A{
    public int iPublic;
    protected int iProtected;
    int iPackage;
    private int iPrivate;
}

public class B extends A implements Serializable{}
In altre parole, abbiamo una classe ereditata da un genitore non serializzabile. È possibile serializzare questa classe e cosa è necessario per questo? Cosa accadrà alle variabili della classe genitore? La risposta è questa. Sì, Bpuoi serializzare un'istanza di una classe. Cosa è necessario per questo? Ma la classe deve Aavere un costruttore senza parametri publico protected. Quindi, durante la deserializzazione, tutte le variabili della classe Averranno inizializzate utilizzando questo costruttore. Le variabili della classe Bverranno inizializzate con i valori del flusso di dati serializzati. Teoricamente è possibile definire in una classe Bi metodi di cui ho parlato all'inizio - readObjecte writeObject, - all'inizio dei quali eseguire la (de-)serializzazione delle variabili della classe Btramite in.defaultReadObject/out.defaultWriteObject, e poi la (de-)serializzazione delle variabili disponibili dalla classe A(nel nostro caso sono iPublic, iProtectede iPackage, se Bè nello stesso pacchetto di A). Tuttavia, a mio avviso, è meglio utilizzare la serializzazione estesa per questo. Il prossimo punto che vorrei toccare è la serializzazione di più oggetti. Diciamo che abbiamo la seguente struttura di classi:
public class A implements Serializable{
    private C c;
    private B b;
    public void setC(C c) {this.c = c;}
    public void setB(B b) {this.b = b;}
    public C getC() {return c;}
    public B getB() {return b;}
}
public class B implements Serializable{
    private C c;
    public void setC(C c) {this.c = c;}
    public C getC() {return c;}
}
public class C implements Serializable{
    private A a;
    private B b;
    public void setA(A a) {this.a = a;}
    public void setB(B b) {this.b = b;}
    public B getB() {return b;}
    public A getA() {return a;}
}
Serializzazione così com'è.  Parte 1 - 2Cosa succede se serializzi un'istanza della classe A? Trascinerà con sé un'istanza della classe B, che a sua volta trascinerà con sé un'istanza Cche ha un riferimento all'istanza A, la stessa con cui tutto ha avuto inizio. Circolo vizioso e ricorsione infinita? Fortunatamente no. Diamo un'occhiata al seguente codice di test:
// initiaizing
A a = new A();
B b = new B();
C c = new C();
// setting references
a.setB(b);
a.setC(c);
b.setC(c);
c.setA(a);
c.setB(b);
// serializing
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
oos.writeObject(b);
oos.writeObject(c);
oos.flush();
oos.close();
// deserializing
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
A a1 = (A)ois.readObject();
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
// testing
System.out.println("a==a1: "+(a==a1));
System.out.println("b==b1: "+(b==b1));
System.out.println("c==c1: "+(c==c1));
System.out.println("a1.getB()==b1: "+(a1.getB()==b1));
System.out.println("a1.getC()==c1: "+(a1.getC()==c1));
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1.getA()==a1: "+(c1.getA()==a1));
System.out.println("c1.getB()==b1: "+(c1.getB()==b1));
Che cosa stiamo facendo? Creiamo un'istanza delle classi A, Be C, forniamo loro collegamenti tra loro e quindi serializziamo ciascuna di esse. Quindi li deserializziamo nuovamente ed eseguiamo una serie di controlli. Cosa accadrà di conseguenza:
a==a1: false
b==b1: false
c==c1: false
a1.getB()==b1: true
a1.getC()==c1: true
b1.getC()==c1: true
c1.getA()==a1: true
c1.getB()==b1: true
Quindi cosa puoi imparare da questo test? Primo. I riferimenti agli oggetti dopo la deserializzazione sono diversi dai riferimenti precedenti. In altre parole, durante la serializzazione/deserializzazione l'oggetto è stato copiato. Questo metodo viene talvolta utilizzato per clonare oggetti. La seconda conclusione è più significativa. Quando si serializzano/deserializzano più oggetti con riferimenti incrociati, tali riferimenti rimangono validi dopo la deserializzazione. In altre parole, se prima della serializzazione puntavano a un oggetto, dopo la deserializzazione punteranno anche a un oggetto. Un altro piccolo test per confermarlo:
B b = new B();
C c = new C();
b.setC(c);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
oos.writeObject(c);
oos.writeObject(c);
oos.writeObject(c);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
C c2 = (C)ois.readObject();
C c3 = (C)ois.readObject();
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1==c2: "+(c1==c2));
System.out.println("c1==c3: "+(c1==c3));
Un oggetto di classe Bha un riferimento a un oggetto di classe C. Una volta serializzato, bviene serializzato insieme a un'istanza della classe С, dopodiché la stessa istanza di c viene serializzata tre volte. Cosa succede dopo la deserializzazione?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
Come puoi vedere, tutti e quattro gli oggetti deserializzati rappresentano in realtà un oggetto: i riferimenti ad esso sono uguali. Esattamente come era prima della serializzazione. Un altro punto interessante: cosa accadrà se implementiamo contemporaneamente Externalizablee Serializable? Come in quella domanda - elefante contro balena - chi sconfiggerà chi? Supererà Externalizable. Il meccanismo di serializzazione ne verifica prima la presenza, e solo dopo, Serializablequindi se la classe B, che implementa Serializable, eredita dalla classe A, che implementa Externalizable, i campi della classe B non verranno serializzati. L'ultimo punto è l'ereditarietà. Quando si eredita da una classe che implementa Serializable, non è necessario intraprendere alcuna azione aggiuntiva. La serializzazione si estenderà anche alla classe figlia. Quando si eredita da una classe che implementa Externalizable, è necessario sovrascrivere i metodi readExternal e writeExternal della classe genitore. Altrimenti i campi della classe figlia non verranno serializzati. In questo caso, dovresti ricordarti di chiamare i metodi genitori, altrimenti i campi genitori non verranno serializzati. * * * Probabilmente abbiamo finito con i dettagli. C’è però una questione che non abbiamo toccato, di natura globale. Vale a dire -

Perché hai bisogno di Esternalizzabile?

Perché abbiamo bisogno della serializzazione avanzata? La risposta è semplice. In primo luogo, offre molta più flessibilità. In secondo luogo, spesso può fornire vantaggi significativi in ​​termini di volume di dati serializzati. In terzo luogo, esiste un aspetto come le prestazioni, di cui parleremo di seguito . Tutto sembra essere chiaro con la flessibilità. Possiamo infatti controllare come vogliamo i processi di serializzazione e deserializzazione, il che ci rende indipendenti da eventuali cambiamenti nella classe (come ho detto poco sopra, i cambiamenti nella classe possono influenzare notevolmente la deserializzazione). Pertanto, voglio dire alcune parole sull'aumento di volume. Diciamo che abbiamo la seguente classe:
public class DateAndTime{

  private short year;
  private byte month;
  private byte day;
  private byte hours;
  private byte minutes;
  private byte seconds;

}
Il resto non è importante. I campi potrebbero essere di tipo int, ma ciò non farebbe altro che aumentare l'effetto dell'esempio. Sebbene in realtà i campi possano essere digitati intper motivi di prestazioni. In ogni caso il punto è chiaro. La classe rappresenta una data e un'ora. Per noi è interessante soprattutto dal punto di vista della serializzazione. Forse la cosa più semplice da fare sarebbe memorizzare un semplice timestamp. È di tipo lungo, cioè una volta serializzato richiederebbe 8 byte. Inoltre, questo approccio richiede metodi per convertire i componenti in un valore e viceversa, ad es. – perdita di produttività. Il vantaggio di questo approccio è una data completamente folle che può essere inserita in 64 bit. Si tratta di un enorme margine di sicurezza, molto spesso non necessario nella realtà. La classe sopra indicata richiederà 2 + 5*1 = 7 byte. Più spese generali per la classe e 6 campi. C'è un modo per comprimere questi dati? Di sicuro. Secondi e minuti sono compresi tra 0 e 59, ovvero per rappresentarli bastano 6 bit invece di 8. Ore – 0-23 (5 bit), giorni – 0-30 (5 bit), mesi – 0-11 (4 bit). Totale, tutto senza tener conto dell'anno - 26 bit. Rimangono ancora 6 bit per la dimensione di int. Teoricamente, in alcuni casi questo potrebbe essere sufficiente per un anno. In caso contrario, l'aggiunta di un altro byte aumenta la dimensione del campo dati a 14 bit, che fornisce un intervallo compreso tra 0 e 16383. Questo è più che sufficiente nelle applicazioni reali. In totale, abbiamo ridotto a 5 byte la dimensione dei dati necessari per memorizzare le informazioni necessarie. Altrimenti fino a 4. Lo svantaggio è lo stesso del caso precedente: se si memorizza la data compressa, sono necessari metodi di conversione. Ma voglio farlo in questo modo: memorizzarlo in campi separati e serializzarlo in formato pacchetto. Questo è dove ha senso usare Externalizable:
// data is packed into 5 bytes:
//  3         2         1
// 10987654321098765432109876543210
// hhhhhmmmmmmssssssdddddMMMMyyyyyy yyyyyyyy
public void writeExternal(ObjectOutput out){
    int packed = 0;
    packed += ((int)hours) << 27;
    packed += ((int)minutes) << 21;
    packed += ((int)seconds) << 15;
    packed += ((int)day) << 10;
    packed += ((int)month) << 6;
    packed += (((int)year) >> 8) & 0x3F;
    out.writeInt(packed);
    out.writeByte((byte)year);
}

public void readExternal(ObjectInput in){
    int packed = in.readInt();
    year = in.readByte() & 0xFF;
    year += (packed & 0x3F) << 8;
    month = (packed >> 6) & 0x0F;
    day = (packed >> 10) & 0x1F;
    seconds = (packed >> 15) & 0x3F;
    minutes = (packed >> 21) & 0x3F;
    hours = (packed >> 27);
}
In realtà, questo è tutto. Dopo la serializzazione, otteniamo un sovraccarico per classe, due campi (invece di 6) e 5 byte di dati. Il che è già nettamente migliore. Ulteriori imballaggi possono essere lasciati alle biblioteche specializzate. L'esempio riportato è molto semplice. Il suo scopo principale è mostrare come è possibile utilizzare la serializzazione avanzata. Anche se, a mio avviso, il possibile guadagno nel volume dei dati serializzati non è il vantaggio principale. Il vantaggio principale, oltre alla flessibilità... (passiamo tranquillamente alla sezione successiva...) Link alla fonte: Serializzazione così com'è
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION