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-класу, також повинні мати конструктори за замовчуванням.
До зустрічі! :)
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ