JavaRush /Java блог /Random UA /Інтерфейс Externalizable у Java

Інтерфейс Externalizable у Java

Стаття з групи Random UA
Вітання! Сьогодні ми продовжимо знайомство із серіалізацією та десеріалізацією об'єктів 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(). Як ми й казали раніше, вся відповідальність за серіалізацію та десеріалізацію лежатиме на програмісті. Але тепер ти можеш вирішити проблему відсутності контролю над цим процесом! Весь процес програмується безпосередньо тобою, що, звичайно, створює набагато більш гнучкий механізм. Крім того, вирішується проблема і з безпекою. Як бачиш, у нас у класі є поле: персональні дані, які не можна зберігати у незашифрованому вигляді. Тепер ми легко можемо написати код, який відповідає цьому обмеженню. Наприклад, додати до нашого класу два простих приватних методи для шифрування та дешифрування секретних даних. Записувати їх у файл та вичитувати з файлу ми будемо саме у зашифрованому вигляді. А решту даних будемо записувати і зчитувати як є :) В результаті наш клас буде виглядати приблизно так:
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класу, теж повинні мати конструктори за замовчуванням. Ось кілька посилань на добрі статті про механізми серіалізації: До зустрічі! :)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ