CodeGym /Blog Java /Random-ES /Interfaz externalizable en Java
Autor
Volodymyr Portianko
Java Engineer at Playtika

Interfaz externalizable en Java

Publicado en el grupo Random-ES
¡Hola! Hoy continuaremos conociendo la serialización y deserialización de objetos Java. En la última lección, conocimos la interfaz del marcador Serializable , revisamos ejemplos de su uso y también aprendimos cómo puede usar la palabra clave transient para controlar el proceso de serialización. Bueno, decir que 'controlamos el proceso' puede ser exagerado. Tenemos una palabra clave, un identificador de versión, y eso es todo. El resto del proceso está oculto dentro de Java y no podemos acceder a él. Por supuesto, en términos de conveniencia, esto es bueno. Pero un programador no debe guiarse únicamente por su propia comodidad, ¿verdad? :) Hay otros factores que debe tener en cuenta. Por eso Serializableno es el único mecanismo para la serialización-deserialización en Java. Hoy nos familiarizaremos con la interfaz externalizable . Pero antes de que comencemos a estudiarlo, es posible que tenga una pregunta razonable: ¿por qué necesitamos otro mecanismo? Serializablehizo su trabajo, y ¿qué hay de malo en la implementación automática de todo el proceso? Y los ejemplos que vimos tampoco fueron complicados. ¿Entonces, cuál es el problema? ¿Por qué necesitamos otra interfaz para esencialmente las mismas tareas? El caso es que Serializabletiene varias carencias. Enumeramos algunos de ellos:
  1. Actuación. La Serializableinterfaz tiene muchas ventajas, pero el alto rendimiento claramente no es una de ellas.

    Presentamos la interfaz Externalizable - 2

    En primer lugar, Serializable la implementación interna de genera una gran cantidad de información de servicio y todo tipo de datos temporales.

    En segundo lugar, Serializable se basa en la API de Reflection (no es necesario que profundice en esto ahora; puede leer más en su tiempo libre, si está interesado). Esto le permite hacer las cosas aparentemente imposibles en Java: por ejemplo, cambiar los valores de los campos privados. CodeGym tiene un excelente artículo sobre la API de Reflection . Puedes leer sobre eso allí.

  2. Flexibilidad. No controlamos el proceso de serialización-deserialización cuando usamos la Serializableinterfaz.

    Por un lado, es muy conveniente, porque si no estamos particularmente preocupados por el rendimiento, entonces parece bueno no tener que escribir código. Pero, ¿qué sucede si realmente necesitamos agregar algunas de nuestras propias funciones (a continuación, brindaremos un ejemplo) a la lógica de serialización?

    Básicamente, lo único que tenemos para controlar el proceso es la transientpalabra clave para excluir algunos datos. Eso es todo. Esa es toda nuestra caja de herramientas :/

  3. Seguridad. Este ítem deriva en parte del ítem anterior.

    No hemos pasado mucho tiempo pensando en esto antes, pero ¿qué pasa si alguna información en su clase no está destinada a los ojos y oídos curiosos de los demás? Un ejemplo simple es una contraseña u otros datos personales del usuario, que en el mundo actual se rigen por un montón de leyes.

    Si usamos Serializable, realmente no podemos hacer nada al respecto. Serializamos todo tal como está.

    Pero si lo hacemos de la manera correcta, debemos cifrar este tipo de datos antes de escribirlos en un archivo o enviarlos a través de una red. Pero Serializableno hace esto posible.

Presentamos la interfaz Externalizable - 3Bueno, por fin veamos cómo se vería la clase si usamos la Externalizableinterfaz.

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 puede ver, ¡tenemos cambios significativos! El principal es obvio: al implementar la Externalizableinterfaz, debe implementar dos métodos requeridos: writeExternal()yreadExternal(). Como dijimos anteriormente, la responsabilidad de la serialización y deserialización recaerá en el programador. ¡Pero ahora puede resolver el problema de no tener control sobre el proceso! Todo el proceso es programado directamente por usted. Naturalmente, esto permite un mecanismo mucho más flexible. Además, el problema con la seguridad está resuelto. Como puede ver, nuestra clase tiene un campo de datos personales que no se puede almacenar sin cifrar. Ahora podemos escribir fácilmente código que satisfaga esta restricción. Por ejemplo, podemos agregar a nuestra clase dos métodos privados simples para cifrar y descifrar datos confidenciales. Escribiremos los datos en el archivo y los leeremos del archivo en forma cifrada. El resto de los datos se escribirán y leerán tal cual :) Como resultado, nuestra clase se ve 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;
   }
}
Implementamos dos métodos que usan los mismos ObjectOutputparámetros ObjectInputque ya conocimos en la lección sobre Serializable. En el momento adecuado, encriptamos o desencriptamos los datos requeridos y usamos los datos encriptados para serializar nuestro objeto. Veamos cómo se ve 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("Paul", "Piper", "Paul Piper's passport data");

       objectOutputStream.writeObject(userInfo);

       objectOutputStream.close();

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

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, nada parece complicado aquí. ¡Deberia de funcionar! Lo ejecutamos y obtenemos... Excepción en el hilo "principal" java.io.InvalidClassException: UserInfo; ningún constructor válido Introducción a la interfaz Externalizable - 4 ¡Vaya! :( ¡Aparentemente, no es tan fácil! El mecanismo de deserialización lanzó una excepción y exigió que creáramos un constructor predeterminado. Me pregunto por qué. Con Serializable, nos las arreglamos sin uno... :/ Aquí encontramos otro matiz importante. El La diferencia entre Serializabley Externalizableradica no solo en el acceso 'ampliado' del programador y la capacidad de controlar el proceso de manera más flexible, sino también en el proceso mismo. Sobre todo, en el mecanismo de deserialización . Al usarSerializable, la memoria simplemente se asigna para el objeto y luego los valores se leen de la secuencia y se usan para establecer los campos del objeto. Si usamos Serializable, ¡no se llama al constructor del objeto! Todo el trabajo ocurre a través de la reflexión (la API de Reflection, que mencionamos brevemente en la última lección). Con Externalizable, el mecanismo de deserialización es diferente. Primero se llama al constructor predeterminado. Solo después de eso se llama al método UserInfodel objeto creado readExternal(). Es responsable de configurar los campos del objeto. Es por eso que cualquier clase que implemente la Externalizableinterfaz debe tener un constructor predeterminado . Agreguemos uno a nuestra UserInfoclase y 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 Paul Piper UserInfo \ firstName = 'Paul', lastName = 'Piper', superSecretInformation = 'Datos del pasaporte de Paul Piper' } ¡ Ahora eso es algo completamente diferente! Primero, la cadena descifrada con información secreta se mostró en la consola. Luego, el objeto que recuperamos del archivo se mostró como una cadena. Así que hemos resuelto con éxito todos los problemas :) El tema de la serialización y deserialización parece simple, pero, como puede ver, las lecciones han sido largas. ¡Y hay mucho más que no hemos cubierto! Todavía hay muchas sutilezas involucradas al usar cada una de estas interfaces. Pero para evitar que su cerebro explote con demasiada información nueva, enumeraré brevemente algunos puntos más importantes y le daré enlaces a lecturas adicionales. Entonces, ¿qué más necesitas saber? Primero , durante la serialización (independientemente de si está usando Serializableo Externalizable), preste atención a staticlas variables. Cuando usa Serializable, estos campos no se serializan en absoluto (y, en consecuencia, sus valores no cambian, porque staticlos campos pertenecen a la clase, no al objeto). Pero cuando usasExternalizable, usted mismo controla el proceso, por lo que técnicamente podría serializarlos. Pero no lo recomendamos, ya que es probable que al hacerlo se creen muchos errores sutiles. En segundo lugar , también debe prestar atención a las variables con el finalmodificador. Cuando usa Serializable, se serializan y deserializan como de costumbre, pero cuando usa Externalizable, ¡es imposible deserializar una finalvariable ! La razón es simple: todos finallos campos se inicializan cuando se llama al constructor predeterminado; después de eso, su valor no se puede cambiar. Por lo tanto, para serializar objetos que tienen finalcampos, use la serialización estándar proporcionada por Serializable. Tercero , cuando usa la herencia, todas las clases descendientes que heredan algúnExternalizableLa clase también debe tener constructores predeterminados. Aquí hay un enlace a un buen artículo sobre los mecanismos de serialización: ¡Hasta la proxima vez! :)
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION