Introduzione alla serializzazione Java

1. Introduzione

La serializzazione è la conversione dello stato di un oggetto in un flusso di byte; la deserializzazione fa l'opposto. Detto in modo diverso, la serializzazione è la conversione di un oggetto Java in un flusso statico (sequenza) di byte che può quindi essere salvato in un database o trasferito su una rete.

2. Serializzazione e deserializzazione

Il processo di serializzazione è indipendente dall'istanza, ovvero gli oggetti possono essere serializzati su una piattaforma e deserializzati su un'altra. Le classi idonee per la serializzazione devono implementare una speciale interfaccia di marker Serializable.

Sia ObjectInputStream che ObjectOutputStream sono classi di alto livello che estendono rispettivamente java.io.InputStream e java.io.OutputStream . ObjectOutputStream può scrivere tipi primitivi e grafici di oggetti in un OutputStream come flusso di byte. Questi flussi possono essere letti successivamente utilizzando ObjectInputStream .

Il metodo più importante in ObjectOutputStream è:

public final void writeObject(Object o) throws IOException;

Che prende un oggetto serializzabile e lo converte in una sequenza (flusso) di byte. Allo stesso modo, il metodo più importante in ObjectInputStream è:

public final Object readObject() throws IOException, ClassNotFoundException;

Che può leggere un flusso di byte e riconvertirlo in un oggetto Java. Questo può quindi essere restituito all'oggetto originale.

Illustriamo la serializzazione con una classe Person . Notare che i campi statici appartengono a una classe (al contrario di un oggetto) e non sono serializzati . Inoltre, nota che possiamo usare la parola chiave transient per ignorare i campi di classe durante la serializzazione:

public class Person implements Serializable { private static final long serialVersionUID = 1L; static String country = "ITALY"; private int age; private String name; transient int height; // getters and setters }

Il test seguente mostra un esempio di salvataggio di un oggetto di tipo Person in un file locale e quindi di leggere nuovamente questo valore:

@Test public void whenSerializingAndDeserializing_ThenObjectIsTheSame() () throws IOException, ClassNotFoundException { Person person = new Person(); person.setAge(20); person.setName("Joe"); FileOutputStream fileOutputStream = new FileOutputStream("yourfile.txt"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(person); objectOutputStream.flush(); objectOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("yourfile.txt"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); Person p2 = (Person) objectInputStream.readObject(); objectInputStream.close(); assertTrue(p2.getAge() == p.getAge()); assertTrue(p2.getName().equals(p.getName())); }

Abbiamo utilizzato ObjectOutputStream per salvare lo stato di questo oggetto in un file utilizzando FileOutputStream . Il file “yourfile.txt” viene creato nella directory del progetto. Questo file viene quindi caricato utilizzando FileInputStream. ObjectInputStream raccoglie questo flusso e lo converte in un nuovo oggetto chiamato p2 .

Infine, testiamo lo stato dell'oggetto caricato e corrisponde allo stato dell'oggetto originale.

Si noti che l'oggetto caricato deve essere cast esplicitamente a un tipo Person .

3. Avvertenze sulla serializzazione Java

Ci sono alcune avvertenze che riguardano la serializzazione in Java.

3.1. Eredità e composizione

Quando una classe implementa l' interfaccia java.io.Serializable , anche tutte le sue sottoclassi sono serializzabili. Al contrario, quando un oggetto ha un riferimento a un altro oggetto, questi oggetti devono implementare l' interfaccia Serializable separatamente, altrimenti verrà lanciata un'eccezione NotSerializableException :

public class Person implements Serializable { private int age; private String name; private Address country; // must be serializable too } 

Se uno dei campi in un oggetto serializzabile è costituito da un array di oggetti, anche tutti questi oggetti devono essere serializzabili, altrimenti verrà generata un'eccezione NotSerializableException .

3.2. UID versione seriale

La JVM associa un numero di versione ( lungo ) a ciascuna classe serializzabile. Viene utilizzato per verificare che gli oggetti salvati e caricati abbiano gli stessi attributi e quindi siano compatibili con la serializzazione.

Questo numero può essere generato automaticamente dalla maggior parte degli IDE e si basa sul nome della classe, sui suoi attributi e sui modificatori di accesso associati. Qualsiasi modifica restituisce un numero diverso e può causare un'eccezione InvalidClassException .

Se una classe serializzabile non dichiara un serialVersionUID , la JVM ne genererà uno automaticamente in fase di esecuzione. Tuttavia, si consiglia vivamente che ogni classe dichiari il proprio serialVersionUID poiché quella generata dipende dal compilatore e quindi potrebbe causare InvalidClassExceptions impreviste .

3.3. Serializzazione personalizzata in Java

Java specifica un modo predefinito in cui gli oggetti possono essere serializzati. Le classi Java possono sovrascrivere questo comportamento predefinito. La serializzazione personalizzata può essere particolarmente utile quando si tenta di serializzare un oggetto che ha alcuni attributi non serializzabili. Questo può essere fatto fornendo due metodi all'interno della classe che vogliamo serializzare:

private void writeObject(ObjectOutputStream out) throws IOException;

e

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

Con questi metodi, possiamo serializzare quegli attributi non serializzabili in altre forme che possono essere serializzate:

public class Employee implements Serializable { private static final long serialVersionUID = 1L; private transient Address address; private Person person; // setters and getters private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); oos.writeObject(address.getHouseNumber()); } private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { ois.defaultReadObject(); Integer houseNumber = (Integer) ois.readObject(); Address a = new Address(); a.setHouseNumber(houseNumber); this.setAddress(a); } }
public class Address { private int houseNumber; // setters and getters }

Il seguente unit test verifica questa serializzazione personalizzata:

@Test public void whenCustomSerializingAndDeserializing_ThenObjectIsTheSame() throws IOException, ClassNotFoundException { Person p = new Person(); p.setAge(20); p.setName("Joe"); Address a = new Address(); a.setHouseNumber(1); Employee e = new Employee(); e.setPerson(p); e.setAddress(a); FileOutputStream fileOutputStream = new FileOutputStream("yourfile2.txt"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(e); objectOutputStream.flush(); objectOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("yourfile2.txt"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); Employee e2 = (Employee) objectInputStream.readObject(); objectInputStream.close(); assertTrue( e2.getPerson().getAge() == e.getPerson().getAge()); assertTrue( e2.getAddress().getHouseNumber() == e.getAddress().getHouseNumber()); }

In questo codice, vediamo come salvare alcuni attributi non serializzabili serializzando Address con serializzazione personalizzata. Notare che dobbiamo contrassegnare gli attributi non serializzabili come transitori per evitare NotSerializableException.

4. Conclusione

In questo breve tutorial, abbiamo esaminato la serializzazione Java, discusso di cose importanti da tenere a mente e mostrato come eseguire la serializzazione personalizzata.

Come sempre, il codice sorgente utilizzato in questo tutorial è disponibile su GitHub.