JavaRush /جاوا بلاگ /Random-UR /Интерфейс Externalizable в Java

Интерфейс Externalizable в Java

گروپ میں شائع ہوا۔
Hello! Сегодня мы продолжим знакомство с сериализацией и десериализацией an objectов Java. В прошлой лекции мы познакомorсь с интерфейсом-маркером Serializable, рассмотрели примеры его использования, а также узнали, How можно управлять процессом сериализации с помощью ключевого слова transient. Ну, «управлять процессом», конечно, громко сказано. У нас есть одно ключевое слово, один идентификатор версии, и в общем-то все. Остальной процесс «зашит» внутрь Java, и к нему доступа нет. С точки зрения удобства это, конечно, хорошо. Но программист в работе должен ориентироваться не только на собственный комфорт, так ведь? :) Есть другие факторы, которые нужно учитывать. Поэтому Serializable — не единственный инструмент для сериализации-десериализации в Java. Сегодня познакомимся с интерфейсом Externalizable. Но еще до того, How мы перешли к его изучению, у тебя мог возникнуть резонный вопрос: а зачем нам еще один инструмент? Serializable справлялся со своей работой, да и автоматическая реализация всего процесса не может не радовать. Примеры, которые мы рассмотрели, тоже не были сложными. Так в чем же дело? Зачем еще один интерфейс для, по сути, той же задачи? Дело в том, что Serializable обладает рядом недостатков. Перечислим некоторые их них:
  1. Производительность. У интерфейса Serializable много плюсов, но высокая производительность явно не из их числа.

Знакомство с интерфейсом Externalizable - 2

Во-первых, внутренний механизм Serializable во время работы генерирует большой объем служебной информации и разного рода временных данных.
Во-вторых (в это можешь сейчас не углубляться и почитать на досуге, если интересно), работа Serializable основана на использовании Reflection API. Эта штуковина позволяет делать, казалось бы, невозможные в Java вещи: например, менять значения приватных полей. На JavaRush есть отличная статья про Reflection API, можешь почитать о ней здесь.

  1. Гибкость.Мы вообще не управляем процессом сериализации-десериализации при использовании интерфейса Serializable.

    С одной стороны, это очень удобно, ведь если нас не особо волнует производительность, возможность не писать code кажется удобной. Но что если нам действительно необходимо добавить Howие-то свои фичи (пример одной из них будет ниже) в логику сериализации?

    По сути, все что у нас есть для управления процессом, — это ключевое слово transient для исключения Howих-либо данных, и все. Такой себе «инструментарий» :/

  2. Безопасность.Этот пункт частично вытекает из предыдущего.

    Мы раньше особо над этим не задумывались, но что делать, если Howая-то информация в твоем классе не предназначена для «чужих ушей» (точнее, глаз)? Простой пример — пароль or другие персональные данные пользователя, которые в современном мире регулируются кучей законов.

    Используя Serializable, мы по факту ничего с этим сделать не можем. Сериализуем все How есть.

    А ведь, по-хорошему, такого рода данные мы должны зашифровать перед записью в файл or передачей по сети. Но Serializable этой возможности не дает.

Знакомство с интерфейсом Externalizable - 3What ж, давай наконец посмотрим, How будет выглядеть класс с использованием интерфейса 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 {

   }
}
Как видишь, у нас появorсь существенные изменения! Главное из них очевидно: при имплементации интерфейса Externalizable ты должен реализовать два обязательных метода — writeExternal() и readExternal(). Как мы и говорor ранее, вся ответственность за сериализацию и десериализацию будет лежать на программисте. Однако теперь ты можешь решить проблему отсутствия контроля над этим процессом! Весь процесс программируется напрямую тобой, что, конечно, создает гораздо более гибкий механизм. Кроме того, решается проблема и c безопасностью. Как видишь, у нас в классе есть поле: персональные данные, которые нельзя хранить в незашифрованном виде. Теперь мы легко можем написать code, соответствующий этому ограничению. К примеру, добавить в наш класс два простых приватных метода для шифрования и дешифрования секретных данных. Записывать их в файл и вычитывать из file мы будем именно в зашифрованном виде. А остальные данные будем записывать и считывать How есть :) В результате наш класс будет выглядеть примерно так:

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;
   }
}
Мы реализовали два метода, которые в качестве параметров используют те же ObjectOutput out и ObjectInput, с которыми мы уже встречались в лекции о Serializable. В нужный момент мы шифруем or расшифровываем необходимые данные, и в таком виде используем их для сериализации нашего an object. Посмотрим, How это будет выглядеть на практике:

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();
      
   }
}
В методах encryptString() и decryptString() мы специально добавor вывод в консоль, чтобы проверить, в Howом виде будут записаны и прочитаны секретные данные. Код выше вывел в консоль строку: SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRh Шифрование удалось! Полное содержание file выглядит так: ¬н sr UserInfoГ!}ҐџC‚ћ xpt Ivant Ivanovt $SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRhx Теперь попробуем использовать написанную нами логику десериализации.

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();

   }
}
Ну, вроде ничего сложного тут нет, должно работать! Запускаем… Exception in thread "main" java.io.InvalidClassException: UserInfo; no valid constructor Знакомство с интерфейсом Externalizable - 4Упс :( Все оказалось не так просто! Механизм десериализации выбросил исключение и потребовал от нас создать конструктор по умолчанию. Интересно, зачем? В Serializable мы обходorсь и без него… :/ Здесь мы подошли к еще одному важному нюансу. Difference между Serializable и Externalizable заключается не только в «расширенном» доступе для программиста и возможность более гибко управлять процессом, но и в самом процессе. Прежде всего, в механизме десериализации. При использовании Serializable под an object просто выделяется память, после чего из потока считываются значения, которыми заполняются все его поля. Если мы используем Serializable, конструктор an object не вызывается! Вся работа производится через рефлексию (Reflection API, который мы мельком упоминали в прошлой лекции). В случае с Externalizable механизм десериализации будет иным. В начале вызывается конструктор по умолчанию. И только потом у созданного an object UserInfo вызывается метод readExternal(), который и отвечает за заполнение полей an object. Именно поэтому любой класс, имплементирующий интерфейс Externalizable, обязан иметь конструктор по умолчанию. Добавим его в наш класс UserInfo и перезапустим code:

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();
   }
}
Вывод в консоль: Ivan Ivanov's passport data UserInfo{firstName='Ivan', lastName='Ivanov', superSecretInformation='Ivan Ivanov's passport data'} Совсем другое дело! В консоль сначала была выведена дешифрованная строка с секретными данными, а после — наш восстановленный из file an object в строковом формате! Вот так мы успешно разрешor все проблемы :) Тема сериализации и десериализации, казалось бы, несложная, но лекции у нас получorсь, How видишь, большими. И это далеко не все! Есть еще множество тонкостей при использовании каждого из этих интерфейсов, но, чтобы сейчас у тебя не взрывался мозг от объема новой информации, я кратко перечислю еще несколько важных моментов и дам ссылки на дополнительное чтение. Итак, что еще тебе нужно знать? Во-первых, при сериализации (неважно, используешь ты Serializable or Externalizable) обращай внимание на переменные static. При использовании Serializable эти поля вообще не сериализуются (и, соответственно, их meaning не меняется, т.к. static поля принадлежат классу, а не an objectу). А вот при использовании Externalizable ты управляешь процессом сам, поэтому технически сделать это можно. Но не рекомендуется, так How это чревато трудноуловимыми ошибками. Во-вторых, внимание стоит также обратить на переменные с модификатором final. При использовании Serializable они сериализуются и десериализуются How обычно, а вот при использовании Externalizable десериализовать final-переменную невозможно! Причина проста: все final-поля инициализируются при вызове конструктора по умолчанию, и после этого их meaning уже невозможно изменить. Поэтому для сериализации an objectов, содержащих final-поля, используй стандартную сериализацию через Serializable. В-третьих, при использовании наследования, все классы-наследники, происходящие от Howого-то Externalizable-класса, тоже должны иметь конструкторы по умолчанию. Вот несколько ссылок на хорошие статьи про механизмы сериализации: До встречи! :)
تبصرے
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION