Serializable
fez o seu trabalho, e como não amar a implementação automática de todo o processo? E os exemplos que vimos também eram descomplicados. Então qual é o problema? Por que precisamos de outra interface para essencialmente as mesmas tarefas? O fato é que Serializable
tem várias deficiências. Listamos alguns deles:
-
Desempenho. A
Serializable
interface tem muitas vantagens, mas o alto desempenho claramente não é uma delas.Primeiro,
Serializable
a implementação interna do gera uma grande quantidade de informações de serviço e todos os tipos de dados temporários.Em segundo lugar,
Serializable
depende da API de reflexão (você não precisa se aprofundar nisso agora; pode ler mais quando quiser, se estiver interessado). Isso permite que você faça coisas aparentemente impossíveis em Java: por exemplo, alterar os valores de campos privados. CodeGym tem um excelente artigo sobre a API Reflection . Você pode ler sobre isso lá. -
Flexibilidade. Não controlamos o processo de serialização-desserialização quando usamos a
Serializable
interface.Por um lado, é muito conveniente, porque se não estamos particularmente preocupados com o desempenho, parece bom não ter que escrever código. Mas e se realmente precisarmos adicionar alguns de nossos próprios recursos (daremos um exemplo abaixo) à lógica de serialização?
Basicamente, tudo o que temos para controlar o processo é a
transient
palavra-chave para excluir alguns dados. É isso. Essa é toda a nossa caixa de ferramentas :/ -
Segurança. Este item deriva em parte do item anterior.
Não gastamos muito tempo pensando sobre isso antes, mas e se algumas informações em sua aula não forem destinadas aos olhares e ouvidos curiosos de outras pessoas? Um exemplo simples é uma senha ou outros dados pessoais do usuário, que no mundo de hoje são regidos por várias leis.
Se usarmos
Serializable
, não podemos fazer nada a respeito. Serializamos tudo como está.Mas se fizermos isso da maneira certa, devemos criptografar esse tipo de dado antes de gravá-lo em um arquivo ou enviá-lo pela rede. Mas
Serializable
não torna isso possível.
Externalizable
interface.
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 {
}
}
Como você pode ver, temos mudanças significativas! O principal é óbvio: ao implementar a Externalizable
interface, você deve implementar dois métodos obrigatórios: writeExternal()
ereadExternal()
. Como dissemos anteriormente, a responsabilidade pela serialização e desserialização será do programador. Mas agora você pode resolver o problema da falta de controle sobre o processo! Todo o processo é programado diretamente por você. Naturalmente, isso permite um mecanismo muito mais flexível. Além disso, o problema com a segurança é resolvido. Como você pode ver, nossa classe tem um campo de dados pessoais que não pode ser armazenado sem criptografia. Agora podemos facilmente escrever um código que satisfaça essa restrição. Por exemplo, podemos adicionar à nossa classe dois métodos privados simples para criptografar e descriptografar dados confidenciais. Vamos gravar os dados no arquivo e lê-los no arquivo de forma criptografada. O resto dos dados serão escritos e lidos como estão :) Como resultado, nossa classe se parece com isto:
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;
}
}
Implementamos dois métodos que utilizam os mesmos parâmetros ObjectOutput
e ObjectInput
que já conhecemos na aula sobre Serializable
. No momento certo, criptografamos ou descriptografamos os dados necessários e usamos os dados criptografados para serializar nosso objeto. Vamos ver como isso fica na prática:
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();
}
}
Nos métodos encryptString()
e decryptString()
, adicionamos especificamente a saída do console para verificar a forma na qual os dados secretos serão gravados e lidos. O código acima exibia a seguinte linha: SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRh A criptografia foi bem-sucedida! O conteúdo completo do arquivo é assim: ¬н sr UserInfoГ!}ҐџC‚ћ xpt Ivant Ivanovt $SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRhx Agora vamos tentar usar nossa lógica de desserialização.
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();
}
}
Bem, nada parece complicado aqui. Deve funcionar! Nós o executamos e obtemos... Exceção no thread "main" java.io.InvalidClassException: UserInfo; nenhum construtor válido Ops! :( Aparentemente, não é tão fácil! O mecanismo de desserialização lançou uma exceção e exigiu que criássemos um construtor padrão. Eu me pergunto por que. Com Serializable
, passamos sem um... :/ Aqui encontramos outra nuance importante. O A diferença entre Serializable
e Externalizable
reside não apenas no acesso 'expandido' do programador e na capacidade de controlar o processo com mais flexibilidade, mas também no próprio processo. Acima de tudo, no mecanismo de desserialização . Ao usarSerializable
, a memória é simplesmente alocada para o objeto e, em seguida, os valores são lidos do fluxo e usados para definir os campos do objeto. Se usarmos Serializable
, o construtor do objeto não é chamado! Todo o trabalho acontece por meio de reflexão (a API de reflexão, que mencionamos brevemente na última lição). Com Externalizable
, o mecanismo de desserialização é diferente. O construtor padrão é chamado primeiro. Somente depois disso é chamado o método UserInfo
do objeto criado. readExternal()
É responsável por configurar os campos do objeto. É por isso que qualquer classe que implementa a Externalizable
interface deve ter um construtor padrão . Vamos adicionar um à nossa UserInfo
classe e executar novamente o código:
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();
}
}
Saída do console: Dados do passaporte de Paul Piper UserInfo \ firstName = 'Paul', lastName = 'Piper', superSecretInformation = 'Dados do passaporte de Paul Piper' } Isso é algo completamente diferente! Primeiro, a string descriptografada com informações secretas foi exibida no console. Em seguida, o objeto que recuperamos do arquivo foi exibido como uma string! Portanto, resolvemos todos os problemas com sucesso :) O tópico de serialização e desserialização parece simples, mas, como você pode ver, as lições foram longas. E há muito mais que não cobrimos! Ainda existem muitas sutilezas envolvidas ao usar cada uma dessas interfaces. Mas, para evitar explodir seu cérebro com o excesso de novas informações, listarei brevemente mais alguns pontos importantes e fornecerei links para leitura adicional. Então, o que mais você precisa saber? Primeiro , durante a serialização (independentemente de você estar usando Serializable
ou Externalizable
), preste atenção às static
variáveis. Quando você usa Serializable
, esses campos não são serializados (e, portanto, seus valores não mudam, porque static
os campos pertencem à classe, não ao objeto). Mas quando você usaExternalizable
, você mesmo controla o processo, portanto, tecnicamente, você pode serializá-los. Mas não o recomendamos, pois isso provavelmente criará muitos bugs sutis. Em segundo lugar , você também deve prestar atenção às variáveis com o final
modificador. Quando você usa Serializable
, eles são serializados e desserializados como de costume, mas quando você usa Externalizable
, é impossível desserializar uma final
variável ! O motivo é simples: todos final
os campos são inicializados quando o construtor padrão é chamado — depois disso, seu valor não pode ser alterado. Portanto, para serializar objetos que possuem final
campos, utilize a serialização padrão fornecida pelo Serializable
. Terceiro , quando você usa herança, todas as classes descendentes que herdam algumExternalizable
classe também deve ter construtores padrão. Aqui está o link para um bom artigo sobre mecanismos de serialização:
Até a próxima vez! :)
GO TO FULL VERSION