Ricerca…


introduzione

Java fornisce un meccanismo, chiamato serializzazione di oggetti in cui un oggetto può essere rappresentato come una sequenza di byte che include i dati dell'oggetto, nonché informazioni sul tipo dell'oggetto e i tipi di dati memorizzati nell'oggetto.

Dopo che un oggetto serializzato è stato scritto in un file, può essere letto dal file e deserializzato cioè, le informazioni sul tipo e i byte che rappresentano l'oggetto ei suoi dati possono essere utilizzati per ricreare l'oggetto in memoria.

Serializzazione di base in Java

Cos'è la serializzazione

La serializzazione è il processo di conversione dello stato di un oggetto (inclusi i suoi riferimenti) in una sequenza di byte, nonché il processo di ricostruzione di quei byte in un oggetto live in un momento futuro. La serializzazione viene utilizzata quando si desidera mantenere l'oggetto. Viene anche utilizzato da Java RMI per passare oggetti tra JVM, come argomenti in un richiamo di metodo da un client a un server o come valori di ritorno da una chiamata di metodo o come eccezioni generate da metodi remoti. In generale, la serializzazione viene utilizzata quando vogliamo che l'oggetto esista oltre la durata della JVM.

java.io.Serializable è un'interfaccia marker (non ha corpo). È appena usato per "contrassegnare" le classi Java come serializzabili.

Il runtime di serializzazione associa a ciascuna classe serializzabile un numero di versione, chiamato serialVersionUID , utilizzato durante la de- serializzazione per verificare che il mittente e il destinatario di un oggetto serializzato abbiano caricato classi per quell'oggetto che sono compatibili con la serializzazione. Se il destinatario ha caricato una classe per l'oggetto che ha un serialVersionUID diverso da quello della classe del mittente corrispondente, la deserializzazione produrrà una InvalidClassException . Una classe serializzabile può dichiarare il proprio serialVersionUID esplicitamente dichiarando un campo denominato serialVersionUID che deve essere static, final, e di tipo long :

ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;

Come rendere una classe idonea per la serializzazione

Per mantenere un oggetto, la rispettiva classe deve implementare l'interfaccia java.io.Serializable .

import java.io.Serializable;

public class SerialClass implements Serializable {

    private static final long serialVersionUID = 1L;  
    private Date currentTime;
    
    public SerialClass() {
        currentTime = Calendar.getInstance().getTime();
    }

    public Date getCurrentTime() {
        return currentTime;
    }
}

Come scrivere un oggetto in un file

Ora dobbiamo scrivere questo oggetto su un file system. Utilizziamo java.io.ObjectOutputStream per questo scopo.

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;

public class PersistSerialClass {

    public static void main(String [] args) {
        String filename = "time.ser";            
        SerialClass time = new SerialClass(); //We will write this object to file system.
        try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename));
            out.writeObject(time); //Write byte stream to file system.
            out.close();
        } catch(IOException ex){
            ex.printStackTrace();
        }
     }
 }

Come ricreare un oggetto dal suo stato serializzato

L'oggetto memorizzato può essere letto dal file system in un secondo momento utilizzando java.io.ObjectInputStream come mostrato di seguito:

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.IOException;
import java.io.java.lang.ClassNotFoundException;

 public class ReadSerialClass {

    public static void main(String [] args) {
        String filename = "time.ser";    
        SerialClass time = null;

        try {
            ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename));
            time = (SerialClass)in.readObject();
            in.close();
        } catch(IOException ex){
            ex.printStackTrace();
        } catch(ClassNotFoundException cnfe){
            cnfe.printStackTrace();
        }
        // print out restored time
        System.out.println("Restored time: " + time.getTime());
     }
 }

La classe serializzata è in forma binaria. La deserializzazione può essere problematica se la definizione della classe cambia: consultare il capitolo sul controllo delle versioni degli oggetti serializzati della specifica di serializzazione Java per i dettagli.

La serializzazione di un oggetto serializza l'intero grafico dell'oggetto di cui è la radice e opera correttamente in presenza di grafici ciclici. Viene fornito un metodo reset() per forzare ObjectOutputStream a dimenticare gli oggetti che sono già stati serializzati.

Campi transitori - Serializzazione

Serializzazione con Gson

La serializzazione con Gson è semplice e genera un JSON corretto.

public class Employe {

    private String firstName;
    private String lastName;
    private int age;      
    private BigDecimal salary;
    private List<String> skills;

    //getters and setters
}

(Serializzazione)

//Skills 
List<String> skills = new LinkedList<String>();
skills.add("leadership");
skills.add("Java Experience");

//Employe
Employe obj = new Employe();
obj.setFirstName("Christian");
obj.setLastName("Lusardi");
obj.setAge(25);
obj.setSalary(new BigDecimal("10000"));
obj.setSkills(skills);

//Serialization process
Gson gson = new Gson();
String json = gson.toJson(obj); //{"firstName":"Christian","lastName":"Lusardi","age":25,"salary":10000,"skills":["leadership","Java Experience"]}

Si noti che non è possibile serializzare oggetti con riferimenti circolari poiché ciò comporterà una ricorsione infinita.

(Deserializzazione)

//it's very simple...
//Assuming that json is the previous String object....

Employe obj2 = gson.fromJson(json, Employe.class); // obj2 is just like obj

Serializzazione con Jackson 2

Di seguito è una implementazione che dimostra come un oggetto può essere serializzato nella sua stringa JSON corrispondente.

class Test {

    private int idx;
    private String name;

    public int getIdx() {
        return idx;
    }

    public void setIdx(int idx) {
        this.idx = idx;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

serializzazione:

Test test = new Test();
test.setIdx(1);
test.setName("abc");
    
ObjectMapper mapper = new ObjectMapper();

String jsonString;
try {
    jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(test);
    System.out.println(jsonString);
} catch (JsonProcessingException ex) {
    // Handle Exception
}

Produzione:

{
  "idx" : 1,
  "name" : "abc"
}

Puoi omettere la stampante Pretty Default se non ne hai bisogno.

La dipendenza usata qui è la seguente:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.6.3</version>
</dependency>

Serializzazione personalizzata

In questo esempio vogliamo creare una classe che genererà e produrrà in console, un numero casuale tra un intervallo di due numeri interi che vengono passati come argomenti durante l'inizializzazione.

    public class SimpleRangeRandom implements Runnable {
    private int min;
    private int max;

    private Thread thread;

    public SimpleRangeRandom(int min, int max){
        this.min = min;
        this.max = max;
        thread = new Thread(this);
        thread.start();
    }

    @Override
 private void WriteObject(ObjectOutputStreamout) throws IO Exception;
    private void ReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
    public void run() {
        while(true) {
            Random rand = new Random();
            System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    }

Ora se vogliamo rendere questa classe serializzabile ci saranno alcuni problemi. La discussione è una delle determinate classi a livello di sistema che non sono serializzabili. Quindi dobbiamo dichiarare il thread come transitorio . In questo modo saremo in grado di serializzare gli oggetti di questa classe, ma avremo ancora un problema. Come puoi vedere nel costruttore impostiamo i valori minimo e massimo del nostro randomizzatore e dopo questo iniziamo il thread che è responsabile della generazione e della stampa del valore casuale. Pertanto, quando si ripristina l'oggetto persistente chiamando il readObject () il costruttore non verrà eseguito di nuovo poiché non vi è creazione di un nuovo oggetto. In tal caso, dobbiamo sviluppare una serializzazione personalizzata fornendo due metodi all'interno della classe. Questi metodi sono:

private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;

Quindi aggiungendo la nostra implementazione in readObject () possiamo iniziare e iniziare la nostra discussione:

class RangeRandom implements Serializable, Runnable {

private int min;
private int max;

private transient Thread thread;
//transient should be any field that either cannot be serialized e.g Thread or any field you do not want serialized

public RangeRandom(int min, int max){
    this.min = min;
    this.max = max;
    thread = new Thread(this);
    thread.start();
}

@Override
public void run() {
    while(true) {
        Random rand = new Random();
        System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

private void writeObject(ObjectOutputStream oos) throws IOException {
    oos.defaultWriteObject();
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    thread = new Thread(this);
    thread.start();
}
}

Ecco il principale per il nostro esempio:

public class Main {
public static void main(String[] args) {
    System.out.println("Hello");
    RangeRandom rangeRandom = new RangeRandom(1,10);

    FileOutputStream fos = null;
    ObjectOutputStream out = null;
    try
    {
        fos = new FileOutputStream("test");
        out = new ObjectOutputStream(fos);
        out.writeObject(rangeRandom);
        out.close();
    }
    catch(IOException ex)
    {
        ex.printStackTrace();
    }


    RangeRandom rangeRandom2 = null;
       FileInputStream fis = null;
       ObjectInputStream in = null;
       try
       {
             fis = new FileInputStream("test");
             in = new ObjectInputStream(fis);
           rangeRandom2 = (RangeRandom)in.readObject();
             in.close();
           }
       catch(IOException ex)
       {
             ex.printStackTrace();
           }
       catch(ClassNotFoundException ex)
       {
             ex.printStackTrace();
           }

}
}

Se esegui il main vedrai che ci sono due thread in esecuzione per ogni istanza RangeRandom e questo perché il metodo Thread.start () è ora sia nel costruttore che in readObject () .

Versioning e serialVersionUID

Quando si implementa l'interfaccia java.io.Serializable per rendere serializzabile una classe, il compilatore cerca un campo static final denominato serialVersionUID di tipo long . Se la classe non ha dichiarato esplicitamente questo campo, il compilatore crea uno di questi campi e lo assegna con un valore che deriva da un calcolo dipendente serialVersionUID di serialVersionUID . Questo calcolo dipende da vari aspetti della classe e segue le specifiche di serializzazione dell'oggetto fornite da Sun. Ma non è garantito che il valore sia lo stesso in tutte le implementazioni del compilatore.

Questo valore viene utilizzato per verificare la compatibilità delle classi rispetto alla serializzazione e questo viene fatto durante la de-serializzazione di un oggetto salvato. Serialization Runtime verifica che serialVersionUID letto dai dati de-serializzati e che serialVersionUID dichiarato nella classe sia esattamente lo stesso. In caso contrario, genera una InvalidClassException .

Si consiglia vivamente di dichiarare e inizializzare in modo esplicito il campo finale statico di tipo long e denominato 'serialVersionUID' in tutte le classi che si desidera rendere serializzabili invece di fare affidamento sul calcolo predefinito del valore per questo campo anche se non si intende usa il controllo delle versioni. Il calcolo del 'serialVersionUID' è estremamente sensibile e può variare da un'implementazione del compilatore a un altro e quindi è possibile che si InvalidClassException l' InvalidClassException anche per la stessa classe solo perché sono state utilizzate diverse implementazioni del compilatore sul mittente e sul destinatario del processo di serializzazione.

public class Example implements Serializable {          
    static final long serialVersionUID = 1L /*or some other value*/;
    //...
}

Finché serialVersionUID è lo stesso, la serializzazione Java può gestire diverse versioni di una classe. I cambiamenti compatibili e incompatibili sono;

Modifiche compatibili

  • Aggiunta di campi: quando la classe da ricostituire ha un campo che non si verifica nel flusso, quel campo nell'oggetto verrà inizializzato sul valore predefinito per il suo tipo. Se è necessaria l'inizializzazione specifica della classe, la classe può fornire un metodo readObject che può inizializzare il campo con valori non predefiniti.
  • Aggiunta di classi: lo stream conterrà la gerarchia di tipi di ciascun oggetto nello stream. Confrontando questa gerarchia nello stream con la classe corrente è possibile rilevare classi aggiuntive. Poiché non vi sono informazioni nel flusso da cui inizializzare l'oggetto, i campi della classe verranno inizializzati sui valori predefiniti.
  • Rimozione classi: il confronto tra la gerarchia di classi nello stream con quella della classe corrente può rilevare che una classe è stata cancellata. In questo caso, i campi e gli oggetti corrispondenti a quella classe vengono letti dallo stream. I campi primitivi vengono scartati, ma vengono creati gli oggetti a cui fa riferimento la classe eliminata, poiché possono essere indirizzati in seguito nello stream. Verranno raccolti automaticamente quando lo stream viene raccolto o ripristinato.
  • Aggiunta di metodi writeObject / readObject: se la versione che legge il flusso ha questi metodi, readObject è previsto, come al solito, di leggere i dati richiesti scritti nello stream dalla serializzazione predefinita. Dovrebbe chiamare defaultReadObject prima di leggere qualsiasi dato facoltativo. Come al solito, il metodo writeObject richiama defaultWriteObject per scrivere i dati richiesti e quindi può scrivere dati opzionali.
  • Aggiunta di java.io.Serializable: equivale ad aggiungere tipi. Non ci saranno valori nello stream per questa classe, quindi i suoi campi verranno inizializzati su valori predefiniti. Il supporto per sottoclassi di classi non serializzabili richiede che il supertipo della classe abbia un costruttore no-arg e la classe stessa venga inizializzata su valori predefiniti. Se il costruttore no-arg non è disponibile, viene generata l'InvalidClassException.
  • Modifica dell'accesso a un campo: i modificatori di accesso public, package, protected e private non hanno alcun effetto sulla possibilità della serializzazione di assegnare valori ai campi.
  • Modifica di un campo da statico a nonstatic o transiente a non transitorio: quando si utilizza la serializzazione predefinita per calcolare i campi serializzabili, questa modifica equivale all'aggiunta di un campo alla classe. Il nuovo campo verrà scritto nello stream, ma le classi precedenti ignoreranno il valore poiché la serializzazione non assegnerà valori a campi statici o transitori.

Cambiamenti incompatibili

  • Eliminazione di campi: se un campo viene eliminato in una classe, lo stream scritto non conterrà il suo valore. Quando il flusso viene letto da una classe precedente, il valore del campo verrà impostato sul valore predefinito perché non è disponibile alcun valore nel flusso. Tuttavia, questo valore predefinito può compromettere negativamente la capacità della versione precedente di adempiere al proprio contratto.
  • Spostamento delle classi in alto o in basso nella gerarchia: questo non può essere consentito poiché i dati nel flusso vengono visualizzati nella sequenza errata.
  • Modifica di un campo non statico in statico o un campo non transitorio in transitorio: quando si fa affidamento sulla serializzazione predefinita, questa modifica equivale all'eliminazione di un campo dalla classe. Questa versione della classe non scriverà quei dati nello stream, quindi non sarà disponibile per essere letto dalle precedenti versioni della classe. Come quando si elimina un campo, il campo della versione precedente verrà inizializzato sul valore predefinito, il che può causare il fallimento della classe in modi imprevisti.
  • Modifica del tipo dichiarato di un campo primitivo: ogni versione della classe scrive i dati con il suo tipo dichiarato. Le versioni precedenti della classe che tentano di leggere il campo non riusciranno perché il tipo di dati nel flusso non corrisponde al tipo del campo.
  • Modifica del metodo writeObject o readObject in modo che non scriva più né legga i dati di campo predefiniti o non li modifichi in modo che tentino di scriverli o leggerli quando la versione precedente no. I dati di campo predefiniti devono essere costantemente visualizzati o non visualizzati nello stream.
  • La modifica di una classe da Serializable a Externalizable o viceversa è una modifica incompatibile poiché il flusso conterrà dati incompatibili con l'implementazione della classe disponibile.
  • Modifica di una classe da un tipo non enum a un tipo enum o viceversa poiché lo stream conterrà dati che sono incompatibili con l'implementazione della classe disponibile.
  • La rimozione di Serializable o Externalizable è una modifica incompatibile poiché, una volta scritta, non fornirà più i campi necessari per le versioni precedenti della classe.
  • L'aggiunta del metodo writeReplace o readResolve a una classe non è compatibile se il comportamento produce un oggetto incompatibile con qualsiasi versione precedente della classe.

Deserializzazione JSON personalizzata con Jackson

Consumiamo API di riposo come formato JSON e quindi unmarshal su un POJO. L'org.codehaus.jackson.map.ObjectMapper di Jackson "funziona" immediatamente e in realtà non facciamo nulla nella maggior parte dei casi. Ma a volte abbiamo bisogno di un deserializzatore personalizzato per soddisfare le nostre esigenze personalizzate e questo tutorial ti guiderà attraverso il processo di creazione del tuo deserializzatore personalizzato.

Diciamo che abbiamo le seguenti entità.

public class User {
    private Long id;
    private String name;
    private String email;

    //getter setter are omitted for clarity 
}

E

public class Program {
    private Long id;
    private String name;
    private User createdBy;
    private String contents;

    //getter setter are omitted for clarity
}

Iniziamo con il serializzare / marescializzare un oggetto.

User user = new User();
user.setId(1L);
user.setEmail("[email protected]");
user.setName("Bazlur Rahman");

Program program = new Program();
program.setId(1L);
program.setName("Program @# 1");
program.setCreatedBy(user);
program.setContents("Some contents");

ObjectMapper objectMapper = new ObjectMapper();

final String json = objectMapper.writeValueAsString (programma); System.out.println (JSON);

Il codice sopra produrrà seguente JSON-

{
    "id": 1,
    "name": "Program @# 1",
    "createdBy": {
        "id": 1,
        "name": "Bazlur Rahman",
        "email": "[email protected]"
    },
    "contents": "Some contents"
}

Ora può fare il contrario molto facilmente. Se disponiamo di questo JSON, possiamo eseguire unmarshal su un oggetto programma usando ObjectMapper come segue:

Ora diciamo, questo non è il caso reale, avremo un JSON diverso da un'API che non corrisponde alla nostra classe Program .

{
"id": 1,
"name": "Program @# 1",
"ownerId": 1
"contents": "Some contents"
}

Guarda la stringa JSON, puoi vedere, ha un campo diverso che è owenerId.

Ora se vuoi serializzare questo JSON come abbiamo fatto prima, avrai delle eccezioni.

Ci sono due modi per evitare le eccezioni e avere questo serializzato -

Ignora i campi sconosciuti

Ignora l' onwerId . Aggiungi la seguente annotazione nella classe Program

@JsonIgnoreProperties(ignoreUnknown = true)
public class Program {}

Scrivi un deserializzatore personalizzato

Ma ci sono casi in cui hai effettivamente bisogno di questo campo owerId . Diciamo che vuoi relazionarlo come un id della classe User .

In tal caso, è necessario scrivere un deserializzatore personalizzato-

Come puoi vedere, devi prima accedere a JsonNode da JonsParser . E quindi puoi facilmente estrarre informazioni da un JsonNode usando il metodo get() . e devi assicurarti del nome del campo. Dovrebbe essere il nome esatto, l'errore di ortografia causerà eccezioni.

E infine, devi registrare ProgramDeserializer su ObjectMapper .

ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Program.class, new ProgramDeserializer());
 
mapper.registerModule(module);
 
String newJsonString = "{\"id\":1,\"name\":\"Program @# 1\",\"ownerId\":1,\"contents\":\"Some contents\"}";
final Program program2 = mapper.readValue(newJsonString, Program.class);

In alternativa, è possibile utilizzare l'annotazione per registrare direttamente il deserializzatore:

@JsonDeserialize(using = ProgramDeserializer.class)
public class Program {
}


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow