Serializable
Ho affrontato il mio lavoro e l'implementazione automatica dell'intero processo non può che rallegrarmi. Anche gli esempi che abbiamo esaminato non erano complicati. Allora, qual è il problema? Perché un'altra interfaccia essenzialmente per la stessa attività? Il fatto è che Serializable
presenta una serie di svantaggi. Ne elenchiamo alcuni:
-
Prestazione. L'interfaccia presenta
Serializable
molti vantaggi, ma le prestazioni elevate chiaramente non sono uno di questi.
Innanzitutto , il meccanismo interno Serializable
genera una grande quantità di informazioni di servizio e vari tipi di dati temporanei durante il funzionamento.
In secondo luogo (non è necessario approfondire questo argomento adesso e leggere a tuo piacimento se sei interessato), il lavoro Serializable
si basa sull'utilizzo dell'API Reflection. Questo aggeggio permette di fare cose che sembrerebbero impossibili in Java: ad esempio, modificare i valori dei campi privati. JavaRush ha pubblicato un eccellente articolo sull'API Reflection , puoi leggerlo qui.
-
Flessibilità. Non controlliamo affatto il processo di serializzazione-deserializzazione quando si utilizza
Serializable
.Da un lato, questo è molto conveniente, perché se non ci interessano veramente le prestazioni, la possibilità di non scrivere codice sembra conveniente. Ma cosa succede se abbiamo davvero bisogno di aggiungere alcune delle nostre funzionalità (un esempio di una di esse sarà riportato di seguito) alla logica di serializzazione?
In sostanza, tutto ciò che dobbiamo controllare il processo è una parola chiave
transient
per escludere alcuni dati, e il gioco è fatto. Una specie di "kit di strumenti" :/ -
Sicurezza. Questo punto segue in parte dal precedente.
Non ci abbiamo pensato molto prima, ma cosa succederebbe se alcune informazioni nella tua classe non fossero destinate alle “orecchie degli altri” (più precisamente, agli occhi)? Un semplice esempio è una password o altri dati personali dell'utente, che nel mondo moderno sono regolati da una serie di leggi.
Utilizzando
Serializable
, in realtà non possiamo farci nulla. Serializziamo tutto così com'è.Ma, in senso buono, dobbiamo crittografare questo tipo di dati prima di scriverli su un file o trasmetterli in rete. Ma
Serializable
non offre questa opportunità.
Externalizable
.
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;
//...конструктор, геттеры, сеттеры, toString()...
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
Come puoi vedere, abbiamo apportato modifiche significative! Quello principale è ovvio: quando si implementa un'interfaccia, Externalizable
è necessario implementare due metodi obbligatori: writeExternal()
e readExternal()
. Come abbiamo detto prima, tutta la responsabilità della serializzazione e deserializzazione ricadrà sul programmatore. Tuttavia, ora puoi risolvere il problema della mancanza di controllo su questo processo! L'intero processo è programmato direttamente da te, il che, ovviamente, crea un meccanismo molto più flessibile. Inoltre, anche il problema della sicurezza è risolto. Come puoi vedere, nella nostra classe abbiamo un campo: dati personali che non possono essere archiviati in chiaro. Ora possiamo scrivere facilmente codice che soddisfi questo vincolo. Ad esempio, aggiungi due semplici metodi privati alla nostra classe per crittografare e decrittografare i dati segreti. Li scriveremo in un file e li leggeremo dal file in forma crittografata. E scriveremo e leggeremo il resto dei dati così come sono :) Di conseguenza, la nostra classe sarà 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 parametri ObjectOutput out
e ObjectInput
che abbiamo già incontrato nella lezione su Serializable
. Al momento giusto, crittifichiamo o decodifichiamo i dati necessari e in questa forma li utilizziamo per serializzare il nostro oggetto. Vediamo come apparirà nella 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("Ivan", "Ivanov", "Ivan Ivanov's passport data");
objectOutputStream.writeObject(userInfo);
objectOutputStream.close();
}
}
Nei metodi encryptString()
e decryptString()
abbiamo aggiunto specificatamente l'output alla console per verificare in quale forma verranno scritti e letti i dati segreti. Il codice sopra restituisce la seguente riga alla console: SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRh Crittografia riuscita! Il contenuto completo del file è simile al seguente: ¬н sr UserInfoГ!}īџC‚ћ xpt Ivant Ivanovt $SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRhx Ora proviamo a utilizzare la logica di deserializzazione che abbiamo scritto.
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, non sembra esserci nulla di complicato qui, dovrebbe funzionare! Eseguiamo... Eccezione nel thread "main" java.io.InvalidClassException: UserInfo; nessun costruttore valido Ops :( Si è rivelato non così semplice! Il meccanismo di deserializzazione ha generato un'eccezione e ci ha richiesto di creare un costruttore predefinito. Mi chiedo perché? Serializable
Siamo riusciti a farne a meno... :/ Qui arriviamo a un'altra sfumatura importante La differenza tra Serializable
e Externalizable
non sta solo nell'accesso "esteso" per il programmatore e nella capacità di gestire in modo più flessibile il processo, ma anche nel processo stesso. Innanzitutto, nel meccanismo di deserializzazione ... Quando viene utilizzata, Serializable
la memoria viene semplicemente allocato per un oggetto, dopodiché vengono letti i valori dallo stream, che riempiono tutti i suoi campi. Se usiamo Serializable
, il costruttore dell'oggetto non viene chiamato! Tutto il lavoro viene svolto tramite la riflessione (Reflection API, di cui abbiamo brevemente parlato nell'ultimo lezione). Nel caso di , il Externalizable
meccanismo di deserializzazione sarà diverso. All'inizio viene chiamato il costruttore predefinito. E solo dopo UserInfo
viene chiamato il metodo dell'oggetto creato readExternal()
, che è responsabile della compilazione dei campi dell'oggetto. Cioè perché ogni classe che implementa l'interfaccia Externalizable
deve avere un costruttore predefinito . Aggiungiamolo alla nostra classe UserInfo
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 Ivan Ivanov UserInfo{firstName='Ivan', lastName='Ivanov', superSecretInformation='dati del passaporto di Ivan Ivanov'} Una questione completamente diversa! Innanzitutto, la stringa decrittografata con i dati segreti è stata inviata alla console, quindi il nostro oggetto è stato recuperato dal file in formato stringa! È così che abbiamo risolto con successo tutti i problemi :) L'argomento della serializzazione e deserializzazione sembra semplice, ma come puoi vedere, le nostre lezioni si sono rivelate lunghe. E non è tutto! Ci sono molte più sottigliezze quando si utilizza ciascuna di queste interfacce, ma affinché ora il tuo cervello non esploda dal volume di nuove informazioni, elencherò brevemente alcuni punti più importanti e fornirò collegamenti a letture aggiuntive. Quindi cos'altro hai bisogno di sapere? Innanzitutto , durante la serializzazione (non importa se usi Serializable
o Externalizable
), presta attenzione alle variabili static
. Quando vengono utilizzati, Serializable
questi campi non vengono affatto serializzati (e, di conseguenza, il loro valore non cambia, poiché static
i campi appartengono alla classe, non all'oggetto). Ma quando lo usi, Externalizable
controlli tu stesso il processo, quindi tecnicamente questo può essere fatto. Ma non è consigliabile, poiché è pieno di errori sottili. In secondo luogo , occorre prestare attenzione anche alle variabili con il modificatore final
. Quando vengono utilizzati, Serializable
vengono serializzati e deserializzati come al solito, ma quando vengono utilizzati è impossibile Externalizable
deserializzare final
una 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 contenenti final
-fields, utilizzare la serializzazione standard tramite Serializable
. In terzo luogo , quando si utilizza l'ereditarietà, anche tutte le classi discendenti che discendono da una Externalizable
classe devono avere costruttori predefiniti. Ecco alcuni collegamenti ad articoli interessanti sui meccanismi di serializzazione:
Ci vediamo! :)
GO TO FULL VERSION