JavaRush /Blog Java /Random-ES /Interfaz externalizable en Java

Interfaz externalizable en Java

Publicado en el grupo Random-ES
¡Hola! Hoy continuaremos nuestra introducción a la serialización y deseriaización de objetos Java. En la última conferencia, conocimos la interfaz del marcador serializable , vimos ejemplos de su uso y también aprendimos cómo controlar el proceso de serialización usando la palabra clave transitoria . Bueno, “gestionar el proceso”, por supuesto, es una palabra fuerte. Tenemos una palabra clave, un ID de versión y eso es básicamente todo. El resto del proceso está "cableado" dentro de Java y no hay acceso a él. Desde el punto de vista de la comodidad, esto es, por supuesto, bueno. Pero un programador en su trabajo no debe centrarse sólo en su propia comodidad, ¿verdad? :) Hay otros factores a considerar. Por tanto, Serializable no es la única herramienta de serialización-deserialización en Java. Hoy nos familiarizaremos con la interfaz externalizable . Pero incluso antes de pasar a estudiarlo, es posible que tengas una pregunta razonable: ¿por qué necesitamos otra herramienta? SerializableHice frente a mi trabajo y la implementación automática de todo el proceso no puede dejar de alegrarme. Los ejemplos que vimos tampoco fueron complicados. Entonces, ¿cuál es el trato? ¿Por qué otra interfaz para esencialmente la misma tarea? El hecho es que Serializabletiene una serie de desventajas. Enumeremos algunos de ellos:
  1. Actuación. La interfaz tiene Serializablemuchas ventajas, pero el alto rendimiento claramente no es una de ellas.

Presentamos la interfaz externalizable - 2

En primer lugar , el mecanismo interno Serializablegenera una gran cantidad de información de servicio y varios tipos de datos temporales durante la operación.
En segundo lugar (no es necesario que entres en esto ahora y leas con tranquilidad si estás interesado), el trabajo Serializablese basa en el uso de la API Reflection. Este artilugio te permite hacer cosas que parecerían imposibles en Java: por ejemplo, cambiar los valores de los campos privados. JavaRush tiene un excelente artículo sobre la API Reflection , puedes leerlo aquí.

  1. Flexibilidad. No controlamos en absoluto el proceso de serialización-deserialización cuando utilizamos Serializable.

    Por un lado, esto es muy conveniente, porque si realmente no nos importa el rendimiento, la capacidad de no escribir código parece conveniente. Pero, ¿qué pasa si realmente necesitamos agregar algunas de nuestras propias características (a continuación se mostrará un ejemplo de una de ellas) a la lógica de serialización?

    Básicamente, lo único que tenemos para controlar el proceso es una palabra clave transientpara excluir algunos datos, y listo. Algo así como un “kit de herramientas” :/

  2. Seguridad. Este punto se desprende parcialmente del anterior.

    No hemos pensado mucho en esto antes, pero ¿qué pasa si cierta información en tu clase no está destinada a “los oídos de otras personas” (más precisamente, a los ojos)? Un ejemplo sencillo es una contraseña u otros datos personales del usuario, que en el mundo moderno están regulados por muchas leyes.

    Usando Serializable, en realidad no podemos hacer nada al respecto. Serializamos todo tal cual.

    Pero, en el buen sentido, debemos cifrar este tipo de datos antes de escribirlos en un archivo o transmitirlos a través de la red. Pero Serializableno da esta oportunidad.

Presentación de la interfaz externalizable - 3Bueno, veamos finalmente cómo se vería una clase usando 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 {

   }
}
Como puedes ver, ¡hemos realizado cambios significativos! El principal es obvio: al implementar una interfaz, Externalizablees necesario implementar dos métodos obligatorios: writeExternal()y readExternal(). Como dijimos anteriormente, toda la responsabilidad de la serialización y deserialización recaerá en el programador. Sin embargo, ¡ahora puedes solucionar el problema de la falta de control sobre este proceso! Todo el proceso lo programa directamente usted, lo que, por supuesto, crea un mecanismo mucho más flexible. Además, también se soluciona el problema de seguridad. Como puede ver, tenemos un campo en nuestra clase: datos personales que no se pueden almacenar sin cifrar. Ahora podemos escribir fácilmente código que cumpla con esta restricción. Por ejemplo, agregue dos métodos privados simples a nuestra clase para cifrar y descifrar datos secretos. Los escribiremos en un archivo y los leeremos del archivo en forma cifrada. Y escribiremos y leeremos el resto de los datos tal como están :) Como resultado, nuestra clase se verá así:
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;
   }
}
Hemos implementado dos métodos que utilizan los mismos parámetros ObjectOutput outy como ObjectInputque ya encontramos en la conferencia sobre Serializable. En el momento adecuado, ciframos o desciframos los datos necesarios y de esta forma los utilizamos para serializar nuestro objeto. Veamos cómo se verá esto en la práctica:
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();

   }
}
En los métodos encryptString()y decryptString(), agregamos específicamente salida a la consola para verificar en qué forma se escribirán y leerán los datos secretos. El código anterior genera la siguiente línea en la consola: SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRh ¡El cifrado se realizó correctamente! El contenido completo del archivo se ve así: ¬н sr UserInfoГ!}ҐџC‚ћ xpt Ivanovt $SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRhx Ahora intentemos usar la lógica de deserialización que escribimos.
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();

   }
}
Bueno, aquí no parece haber nada complicado, ¡debería funcionar! Ejecutemos... Excepción en el hilo "principal" java.io.InvalidClassException: UserInfo; no hay constructor válido Presentación de la interfaz externalizable - 4 Ups :( ¡Resultó que no era tan simple! El mecanismo de deserialización arrojó una excepción y nos obligó a crear un constructor predeterminado. ¿Me pregunto por qué? SerializableNos las arreglamos sin él... :/ Aquí llegamos a otro matiz importante ... La diferencia entre Serializabley Externalizableradica no solo en el acceso "ampliado" para el programador y la capacidad de administrar el proceso de manera más flexible, sino también en el proceso mismo. En primer lugar, en el mecanismo de deserialización ... Cuando se usa, Serializablela memoria es simplemente asignado para un objeto, después de lo cual se leen los valores de la secuencia, que llenan todos sus campos. Si usamos Serializable, ¡no se llama al constructor del objeto! Todo el trabajo se realiza a través de la reflexión (API de reflexión, que mencionamos brevemente en el último conferencia). En el caso de , el Externalizablemecanismo de deserialización será diferente. Al principio, se llama al constructor predeterminado. Y solo entonces UserInfose llama al método del objeto creado readExternal(), que es responsable de completar los campos del objeto. Es decir Por qué cualquier clase que implemente la interfaz Externalizabledebe tener un constructor predeterminado . Agreguémoslo a nuestra clase UserInfoy volvamos a ejecutar el 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();
   }
}
Salida de la consola: datos del pasaporte de Ivan Ivanov UserInfo{firstName='Ivan', lastName='Ivanov', superSecretInformation='datos del pasaporte de Ivan Ivanov'} ¡ Un asunto completamente diferente! Primero, la cadena descifrada con datos secretos se envió a la consola y luego nuestro objeto se recuperó del archivo en formato de cadena. Así resolvimos con éxito todos los problemas :) El tema de la serialización y deserialización parece simple, pero como puede ver, nuestras conferencias resultaron largas. ¡Y eso no es todo! Hay muchas más sutilezas al usar cada una de estas interfaces, pero para que ahora su cerebro no explote por la cantidad de información nueva, enumeraré brevemente algunos puntos más importantes y proporcionaré enlaces a lecturas adicionales. Entonces, ¿qué más necesitas saber? En primer lugar , al serializar (no importa si usa Serializableo Externalizable), preste atención a las variables static. Cuando se usan, Serializableestos campos no se serializan en absoluto (y, en consecuencia, su valor no cambia, ya que staticlos campos pertenecen a la clase, no al objeto). Pero al usarlo, Externalizableusted mismo controla el proceso, por lo que técnicamente esto se puede hacer. Pero no se recomienda, ya que está plagado de errores sutiles. En segundo lugar , también se debe prestar atención a las variables con el modificador final. Cuando se usan, Serializablese serializan y deserializan como de costumbre, pero cuando se usan, ¡es imposible Externalizabledeserializar finaluna variable ! La razón es simple: todos finallos campos se inicializan cuando se llama al constructor predeterminado y después de eso su valor no se puede cambiar. Por lo tanto, para serializar objetos que contienen finalcampos, utilice la serialización estándar mediante Serializable. En tercer lugar , cuando se utiliza la herencia, todas las clases descendientes que descienden de alguna Externalizableclase también deben tener constructores predeterminados. Aquí hay algunos enlaces a buenos artículos sobre mecanismos de serialización: ¡Nos vemos! :)
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION