Привет! Сегодня мы продолжим знакомство с сериализацией и десериализацией объектов Java.
Знакомство с интерфейсом Externalizable - 1
В прошлой лекции мы познакомились с интерфейсом-маркером Serializable, рассмотрели примеры его использования, а также узнали, как можно управлять процессом сериализации с помощью ключевого слова transient. Ну, «управлять процессом», конечно, громко сказано. У нас есть одно ключевое слово, один идентификатор версии, и в общем-то все. Остальной процесс «зашит» внутрь Java, и к нему доступа нет. С точки зрения удобства это, конечно, хорошо. Но программист в работе должен ориентироваться не только на собственный комфорт, так ведь? :) Есть другие факторы, которые нужно учитывать. Поэтому Serializable — не единственный инструмент для сериализации-десериализации в Java. Сегодня познакомимся с интерфейсом Externalizable. Но еще до того, как мы перешли к его изучению, у тебя мог возникнуть резонный вопрос: а зачем нам еще один инструмент? Serializable справлялся со своей работой, да и автоматическая реализация всего процесса не может не радовать. Примеры, которые мы рассмотрели, тоже не были сложными. Так в чем же дело? Зачем еще один интерфейс для, по сути, той же задачи? Дело в том, что Serializable обладает рядом недостатков. Перечислим некоторые их них:
  1. Производительность. У интерфейса Serializable много плюсов, но высокая производительность явно не из их числа.

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

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

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

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

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

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

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

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

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

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

   //...конструктор, геттеры, сеттеры, toString()...

   @Override
   public void writeExternal(ObjectOutput out) throws IOException {

   }

   @Override
   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

   }
}
Как видишь, у нас появились существенные изменения! Главное из них очевидно: при имплементации интерфейса Externalizable ты должен реализовать два обязательных метода — writeExternal() и readExternal(). Как мы и говорили ранее, вся ответственность за сериализацию и десериализацию будет лежать на программисте. Однако теперь ты можешь решить проблему отсутствия контроля над этим процессом! Весь процесс программируется напрямую тобой, что, конечно, создает гораздо более гибкий механизм. Кроме того, решается проблема и c безопасностью. Как видишь, у нас в классе есть поле: персональные данные, которые нельзя хранить в незашифрованном виде. Теперь мы легко можем написать код, соответствующий этому ограничению. К примеру, добавить в наш класс два простых приватных метода для шифрования и дешифрования секретных данных. Записывать их в файл и вычитывать из файла мы будем именно в зашифрованном виде. А остальные данные будем записывать и считывать как есть :) В результате наш класс будет выглядеть примерно так:
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;

   @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. В нужный момент мы шифруем или расшифровываем необходимые данные, и в таком виде используем их для сериализации нашего объекта. Посмотрим, как это будет выглядеть на практике:
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() мы специально добавили вывод в консоль, чтобы проверить, в каком виде будут записаны и прочитаны секретные данные. Код выше вывел в консоль строку: SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRh Шифрование удалось! Полное содержание файла выглядит так: ¬н 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 мы обходились и без него… :/ Здесь мы подошли к еще одному важному нюансу. Разница между Serializable и Externalizable заключается не только в «расширенном» доступе для программиста и возможностb более гибко управлять процессом, но и в самом процессе. Прежде всего, в механизме десериализации. При использовании Serializable под объект просто выделяется память, после чего из потока считываются значения, которыми заполняются все его поля. Если мы используем Serializable, конструктор объекта не вызывается! Вся работа производится через рефлексию (Reflection API, который мы мельком упоминали в прошлой лекции). В случае с Externalizable механизм десериализации будет иным. В начале вызывается конструктор по умолчанию. И только потом у созданного объекта UserInfo вызывается метод readExternal(), который и отвечает за заполнение полей объекта. Именно поэтому любой класс, имплементирующий интерфейс Externalizable, обязан иметь конструктор по умолчанию. Добавим его в наш класс UserInfo и перезапустим код:
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'} Совсем другое дело! В консоль сначала была выведена дешифрованная строка с секретными данными, а после — наш восстановленный из файла объект в строковом формате! Вот так мы успешно разрешили все проблемы :) Тема сериализации и десериализации, казалось бы, несложная, но лекции у нас получились, как видишь, большими. И это далеко не все! Есть еще множество тонкостей при использовании каждого из этих интерфейсов, но, чтобы сейчас у тебя не взрывался мозг от объема новой информации, я кратко перечислю еще несколько важных моментов и дам ссылки на дополнительное чтение. Итак, что еще тебе нужно знать? Во-первых, при сериализации (неважно, используешь ты Serializable или Externalizable) обращай внимание на переменные static. При использовании Serializable эти поля вообще не сериализуются (и, соответственно, их значение не меняется, т.к. static поля принадлежат классу, а не объекту). А вот при использовании Externalizable ты управляешь процессом сам, поэтому технически сделать это можно. Но не рекомендуется, так как это чревато трудноуловимыми ошибками. Во-вторых, внимание стоит также обратить на переменные с модификатором final. При использовании Serializable они сериализуются и десериализуются как обычно, а вот при использовании Externalizable десериализовать final-переменную невозможно! Причина проста: все final-поля инициализируются при вызове конструктора по умолчанию, и после этого их значение уже невозможно изменить. Поэтому для сериализации объектов, содержащих final-поля, используй стандартную сериализацию через Serializable. В-третьих, при использовании наследования, все классы-наследники, происходящие от какого-то Externalizable-класса, тоже должны иметь конструкторы по умолчанию. Вот несколько ссылок на хорошие статьи про механизмы сериализации: До встречи! :)