Serializable
ha fatto il suo lavoro, e cosa c'è da non amare dell'implementazione automatica dell'intero processo? E anche gli esempi che abbiamo visto erano semplici. Allora, qual'è il problema? Perché abbiamo bisogno di un'altra interfaccia per essenzialmente le stesse attività? Il fatto è che Serializable
ha diversi difetti. Ne elenchiamo alcuni:
-
Prestazione. L'
Serializable
interfaccia ha molti vantaggi, ma chiaramente le alte prestazioni non sono tra queste.Innanzitutto,
Serializable
l'implementazione interna di genera una grande quantità di informazioni sul servizio e tutti i tipi di dati temporanei.In secondo luogo,
Serializable
si basa sull'API Reflection (non è necessario approfondire questo argomento in questo momento; puoi leggere di più a tuo piacimento, se sei interessato). Questa cosa ti consente di fare le cose apparentemente impossibili in Java: ad esempio, modificare i valori dei campi privati. CodeGym ha un eccellente articolo sull'API Reflection . Puoi leggerlo lì. -
Flessibilità. Non controlliamo il processo di serializzazione-deserializzazione quando utilizziamo l'
Serializable
interfaccia.Da un lato, è molto conveniente, perché se non siamo particolarmente preoccupati per le prestazioni, allora sembra bello non dover scrivere codice. Ma cosa succede se abbiamo davvero bisogno di aggiungere alcune delle nostre funzionalità (forniremo un esempio di seguito) alla logica di serializzazione?
Fondamentalmente, tutto ciò che abbiamo per controllare il processo è la
transient
parola chiave per escludere alcuni dati. Questo è tutto. Questa è la nostra intera cassetta degli attrezzi :/ -
Sicurezza. Tale voce deriva in parte dalla voce precedente.
Non abbiamo passato molto tempo a pensarci prima, ma cosa succede se alcune informazioni nella tua classe non sono destinate agli occhi e alle orecchie indiscreti degli altri? Un semplice esempio è una password o altri dati personali dell'utente, che nel mondo di oggi sono regolati da una serie di leggi.
Se usiamo
Serializable
, non possiamo davvero farci niente. Serializziamo tutto così com'è.Ma se lo facciamo nel modo giusto, dobbiamo crittografare questo tipo di dati prima di scriverli su un file o inviarli su una rete. Ma
Serializable
non lo rende possibile.
Externalizable
interfaccia.
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class UserInfo implements Externalizable {
private String firstName;
private String lastName;
private String superSecretInformation;
private static final long SERIAL_VERSION_UID = 1L;
// ...constructor, getters, setters, toString()...
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
Come puoi vedere, abbiamo cambiamenti significativi! Il principale è ovvio: quando si implementa l' Externalizable
interfaccia, è necessario implementare due metodi richiesti: writeExternal()
ereadExternal()
. Come abbiamo detto in precedenza, la responsabilità della serializzazione e della deserializzazione ricadrà sul programmatore. Ma ora puoi risolvere il problema dell'assenza di controllo sul processo! L'intero processo è programmato direttamente da te. Naturalmente, questo consente un meccanismo molto più flessibile. Inoltre, il problema con la sicurezza è risolto. Come puoi vedere, la nostra classe ha un campo di dati personali che non può essere archiviato in chiaro. Ora possiamo facilmente scrivere codice che soddisfi questo vincolo. Ad esempio, possiamo aggiungere alla nostra classe due semplici metodi privati per crittografare e decrittografare dati sensibili. Scriveremo i dati nel file e li leggeremo dal file in forma crittografata. Il resto dei dati verrà scritto e letto così com'è :) Di conseguenza, la nostra classe è simile a questa:
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Base64;
public class UserInfo implements Externalizable {
private String firstName;
private String lastName;
private String superSecretInformation;
private static final long serialVersionUID = 1L;
public UserInfo() {
}
public UserInfo(String firstName, String lastName, String superSecretInformation) {
this.firstName = firstName;
this.lastName = lastName;
this.superSecretInformation = superSecretInformation;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(this.getFirstName());
out.writeObject(this.getLastName());
out.writeObject(this.encryptString(this.getSuperSecretInformation()));
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
firstName = (String) in.readObject();
lastName = (String) in.readObject();
superSecretInformation = this.decryptString((String) in.readObject());
}
private String encryptString(String data) {
String encryptedData = Base64.getEncoder().encodeToString(data.getBytes());
System.out.println(encryptedData);
return encryptedData;
}
private String decryptString(String data) {
String decrypted = new String(Base64.getDecoder().decode(data));
System.out.println(decrypted);
return decrypted;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getSuperSecretInformation() {
return superSecretInformation;
}
}
Abbiamo implementato due metodi che utilizzano gli stessi ObjectOutput
parametri ObjectInput
che abbiamo già incontrato nella lezione su Serializable
. Al momento giusto, crittografiamo o decifriamo i dati richiesti e utilizziamo i dati crittografati per serializzare il nostro oggetto. Vediamo come appare in pratica:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
UserInfo userInfo = new UserInfo("Paul", "Piper", "Paul Piper's passport data");
objectOutputStream.writeObject(userInfo);
objectOutputStream.close();
}
}
Nei metodi encryptString()
e decryptString()
abbiamo aggiunto in modo specifico l'output della console per verificare la forma in cui i dati segreti verranno scritti e letti. Il codice sopra mostrava la seguente riga: SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRh La crittografia è riuscita! Il contenuto completo del file è simile a questo: ¬н sr UserInfoГ!}ҐџC‚ћ xpt Ivanovt $SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRhx Ora proviamo a usare la nostra logica di deserializzazione.
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
UserInfo userInfo = (UserInfo) objectInputStream.readObject();
System.out.println(userInfo);
objectInputStream.close();
}
}
Bene, niente sembra complicato qui. Dovrebbe funzionare! Lo eseguiamo e otteniamo... Eccezione nel thread "main" java.io.InvalidClassException: UserInfo; nessun costruttore valido Spiacenti! :( Apparentemente, non è così facile! Il meccanismo di deserializzazione ha generato un'eccezione e ha richiesto la creazione di un costruttore predefinito. Mi chiedo perché. Con Serializable
, ce la siamo cavata senza uno... :/ Qui abbiamo incontrato un'altra sfumatura importante. Il differenza tra Serializable
e Externalizable
risiede non solo nell'accesso "espanso" del programmatore e nella capacità di controllare in modo più flessibile il processo, ma anche nel processo stesso e soprattutto nel meccanismo di deserializzazione .Serializable
, la memoria viene semplicemente allocata per l'oggetto, quindi i valori vengono letti dal flusso e usati per impostare i campi dell'oggetto. Se usiamo Serializable
, il costruttore dell'oggetto non viene chiamato! Tutto il lavoro avviene attraverso la riflessione (la Reflection API, che abbiamo brevemente menzionato nell'ultima lezione). Con Externalizable
, il meccanismo di deserializzazione è diverso. Il costruttore predefinito viene chiamato per primo. Solo dopo viene chiamato il metodo UserInfo
dell'oggetto creato readExternal()
. È responsabile dell'impostazione dei campi dell'oggetto. Ecco perché qualsiasi classe che implementa l' Externalizable
interfaccia deve avere un costruttore predefinito . Aggiungiamone uno alla nostra UserInfo
classe ed eseguiamo nuovamente il codice:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
UserInfo userInfo = (UserInfo) objectInputStream.readObject();
System.out.println(userInfo);
objectInputStream.close();
}
}
Output della console: dati del passaporto di Paul Piper UserInfo \ firstName = 'Paul', lastName = 'Piper', superSecretInformation = 'dati del passaporto di Paul Piper' } Ora è qualcosa di completamente diverso! Innanzitutto, la stringa decrittografata con informazioni segrete è stata visualizzata sulla console. Quindi l'oggetto che abbiamo recuperato dal file è stato visualizzato come una stringa! Quindi abbiamo risolto con successo tutti i problemi :) L'argomento della serializzazione e deserializzazione sembra semplice, ma, come puoi vedere, le lezioni sono state lunghe. E c'è molto di più che non abbiamo coperto! Ci sono ancora molte sottigliezze coinvolte nell'utilizzo di ciascuna di queste interfacce. Ma per evitare di farti esplodere il cervello da troppe nuove informazioni, elencherò brevemente alcuni punti più importanti e ti fornirò collegamenti a letture aggiuntive. Allora, cos'altro hai bisogno di sapere? Innanzitutto , durante la serializzazione (indipendentemente dal fatto che tu stia utilizzando Serializable
o Externalizable
), fai attenzione alle static
variabili. Quando usi Serializable
, questi campi non sono affatto serializzati (e, di conseguenza, i loro valori non cambiano, perché static
i campi appartengono alla classe, non all'oggetto). Ma quando usiExternalizable
, controlli tu stesso il processo, quindi tecnicamente potresti serializzarli. Ma non lo consigliamo, poiché è probabile che ciò crei molti bug sottili. In secondo luogo , dovresti anche prestare attenzione alle variabili con il final
modificatore. Quando usi Serializable
, vengono serializzati e deserializzati come al solito, ma quando usi Externalizable
, è impossibile deserializzare una final
variabile ! Il motivo è semplice: tutti final
i campi vengono inizializzati quando viene chiamato il costruttore predefinito, dopodiché il loro valore non può essere modificato. Pertanto, per serializzare oggetti con final
campi, utilizzare la serializzazione standard fornita da Serializable
. Terzo , quando usi l'ereditarietà, tutte le classi discendenti che ne ereditano alcuneExternalizable
class deve avere anche costruttori predefiniti. Ecco il collegamento a un buon articolo sui meccanismi di serializzazione:
Fino alla prossima volta! :)
GO TO FULL VERSION