JavaRush/Java блог/Java Developer/Интерфейс Externalizable в Java
Автор
Владимир Портянко
Java-разработчик в Playtika

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

Статья из группы Java Developer
участников
Привет! Сегодня мы продолжим знакомство с сериализацией и десериализацией объектов Java. В прошлой лекции мы познакомились с интерфейсом-маркером 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 SERIAL_VERSION_UID = 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;

   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. В нужный момент мы шифруем или расшифровываем необходимые данные, и в таком виде используем их для сериализации нашего объекта. Посмотрим, как это будет выглядеть на практике:
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 заключается не только в «расширенном» доступе для программиста и возможность более гибко управлять процессом, но и в самом процессе. Прежде всего, в механизме десериализации. При использовании 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-класса, тоже должны иметь конструкторы по умолчанию. Вот несколько ссылок на хорошие статьи про механизмы сериализации: До встречи! :)
Комментарии (107)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Zhandos
Уровень 30
12 апреля, 12:29
где и как вызывается методы writeExternal и readExternal мы же специально их не вызывали?
Viktoriya
Уровень 43
19 января, 17:13
Thank you so much!
Victor Zav
Уровень 33
27 августа 2023, 21:44
Отличная статья 👍 для вывода инфы о юзере можно написать что то вроде: @Override public String toString() { return "UserInfo{" + "\n" + "firstName: " + firstName + "\n" + "lastName: " + lastName + "\n" + "superSecretInformation: " + superSecretInformation + '}'; }
Ислам
Уровень 33
15 июля 2023, 10:43
Nice
RoMANzhula
Уровень 32
Expert
1 мая 2023, 14:00
Не забываем переопределять метод toString(), чтоб в консоль выводить данные.
Ramazan
Уровень 36
Expert
18 января 2023, 13:33
Не совсем понял, про исключение про десериализации объекта. Ведь в коде выше, перед тем как сереализовать Класс UserInfo содержит конструктор по умолчанию: public UserInfo() { }
Gans Electro
Уровень 50
23 апреля 2023, 15:21
Именно. Там код правильный. Если его не было то было бы исключение. Ошибка на том кто писал
dvazhdydva
Уровень 22
2 ноября 2022, 16:44
кто знает, куда пропал skipy.ru? последние записи датиру2тся 2014м годом. уж очень понравился его стиль написания статей по джаве (и не только). наде2сь с ним все хорошо.
Misha Saharin
Уровень 111
Expert
16 ноября 2022, 13:22
писать пробовали skiрy@skipy.ru? читать ходить
dvazhdydva
Уровень 22
22 ноября 2022, 11:26
я был и там и там. жж не обновляется с 2014, a джаватолк нихт арбайтн. признаться честно, я не писал ему.
Goldman Co
Уровень 1
27 ноября 2022, 12:31
Брат, он умер...
Стас
Уровень 38
13 декабря 2022, 14:47
Чё, совсем?
Андрей звукооператор в ДК при заводе
28 февраля 2023, 22:08
реально, не шутка? печально, отличные статьи ((
Anonymous #3045761
Уровень 51
22 октября 2022, 18:08
Что это значит?
При использовании Serializable под объект просто выделяется память,
после чего из потока считываются значения,
которыми заполняются все его поля.
Если мы используем Serializable, конструктор объекта не вызывается!
О каком объекте идет речь?
fedyaka
Уровень 36
24 октября 2022, 13:29
Ну смотри, у нас есть объект UserInfo, в реалях java есть 2 варианта как создавать объект, через конструктор, то есть стандартный способ и через рефлексию. В данном случае используется рефлексия. У рефлексии есть открытый доступ к любому классу, к любой переменной и методу внутри этого класса, даже если он приватный. То есть он имеет доступ ко всему внутри класса, то есть любой информации о классе. При помощи этого класса он создаёт объекты на прямую выделяя под неё память, записывая абсолютно любые данные в любые переменные(если это не ломает логику в дальнейшей работе этого объекта) и отпускает в виде обычного объекта в руки программисту. По сути примерно так и работает рефлексия, на неё не действуют принципы ООП, то есть он способен обходить инкапсуляцию, создание через конструкторы и т.п. Надеюсь достаточно ясно объяснил на что способна рефлексия и как это примерно работает😀
7 ноября 2022, 22:49
UserInfo userInfo
Михаил Заика
Уровень 50
27 января 2022, 06:58
Всё ещё не работает первая ссылка из представленных в конце лекции.
Anna Avilova architect
17 марта 2022, 12:49
ссыль заработала: Сериализация как она есть
Anonymous #2372013
Уровень 26
21 января 2022, 09:29
Уважаемые админы! Обратите внимание,что срок действия ссылки на статью "Сериализация как она есть" истек http://www.skipy.ru/technics/serialization.html#performance
Anna Avilova architect
17 марта 2022, 12:49