Serializable
справлявся зі своєю роботою, та й автоматична реалізація всього процесу не може не тішити. Приклади, які ми розглянули, також не були складними. Так у чому ж справа? Навіщо ще один інтерфейс для по суті того ж завдання? Справа в тому, що Serializable
має ряд недоліків. Перерахуємо деякі з них:
-
Продуктивність. У інтерфейсу
Serializable
багато плюсів, але висока продуктивність явно не з-поміж них.
По-перше , внутрішній механізм Serializable
під час роботи генерує великий обсяг службової інформації та різного роду тимчасових даних.
По-друге (у це можеш зараз не заглиблюватися та почитати на дозвіллі, якщо цікаво), робота Serializable
заснована на використанні Reflection API. Ця штуковина дозволяє робити, здавалося б, неможливі в Java речі: наприклад, змінювати значення приватних полів. На JavaRush є чудова стаття про Reflection API , можеш почитати про неї тут.
-
Гнучкість. Ми взагалі не керуємо процесом серіалізації-десеріалізації при використанні інтерфейсу
Serializable
.З одного боку, це дуже зручно, адже якщо нас не особливо турбує продуктивність, можливість не писати код видається зручною. Але якщо нам дійсно необхідно додати якісь свої фічі (приклад однієї з них буде нижче) в логіку серіалізації?
По суті, все, що ми маємо для управління процесом, — це ключове слово
transient
для виключення будь-яких даних, і все. Такий собі інструментарій :/ -
Безпека. Цей пункт частково випливає із попереднього.
Ми раніше над цим особливо не замислювалися, але що робити, якщо якась інформація в твоєму класі не призначена для «чужих вух» (точніше, очей)? Простий приклад – пароль або інші персональні дані користувача, які у сучасному світі регулюються купою законів.
Використовуючи
Serializable
ми за фактом нічого з цим зробити не можемо. Серіалізуємо все як є.Адже, по-хорошому, такі дані ми повинні зашифрувати перед записом у файл чи передачею через мережу. Але
Serializable
цієї можливості не надає.
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 Упс :( Все виявилося не так просто! Механізм десеріалізації викинув виняток і зажадав від нас створити конструктор за замовчуванням. Цікаво, навіщо? Ми 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
класу, теж повинні мати конструктори за замовчуванням. Ось кілька посилань на добрі статті про механізми серіалізації:
До зустрічі! :)
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ