1. Вступ
Автоматична серіалізація — це як автопілот у літаку: чудово працює, поки все йде за планом. Щойно з’являються особливі умови — стає зрозуміло, що простого механізму вже недостатньо. Уявіть, що потрібно зберегти об’єкт, але не всі його поля: якісь дані тимчасові, а якісь надто чутливі, щоб їх записувати у файл. Або навпаки — під час збереження потрібно додати щось своє: наприклад, версію чи контрольну суму. Буває й так, що перед записом або завантаженням даних потрібно виконати перевірку чи перетворення. А іноді завдання ще складніше: забезпечити сумісність із попередніми версіями класу, якщо його структура з часом змінилася.
Ось у таких ситуаціях і стає зрозуміло: однією стандартною серіалізацією не обійтися. Потрібно брати керування у власні руки.
Спеціальні методи серіалізації: writeObject і readObject
У Java є два спеціальні методи, які дають змогу повністю контролювати процес серіалізації та десеріалізації об’єкта:
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
Важливо!
- Методи мають бути саме private (не public, не protected, не package-private).
- Сигнатури мають точно збігатися з наведеними вище.
- Якщо ці методи оголошені у вашому класі, вони будуть викликані замість стандартної серіалізації/десеріалізації.
Як це працює?
Коли ви викликаєте ObjectOutputStream.writeObject(obj), JVM спочатку шукає в класі obj метод private void writeObject(ObjectOutputStream). Якщо він є — викликається саме він. Аналогічно, під час десеріалізації викликається private void readObject(ObjectInputStream).
Якщо методи не оголошені, використовується стандартна серіалізація.
Як влаштовані writeObject і readObject
Сигнатури методів
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
Усередині цих методів ви зобов’язані викликати:
- out.defaultWriteObject(); — для серіалізації стандартних (не transient) полів суперкласу та поточного класу.
- in.defaultReadObject(); — для десеріалізації стандартних полів.
Якщо ви не викличете ці методи, то стандартні поля серіалізовані не будуть — і під час десеріалізації об’єкт виявиться «порожнім». Це як забути покласти паспорт у валізу: формально ви приїхали, але довести, хто ви, не зможете.
2. Приклад: додаємо контрольну суму під час серіалізації
Розгляньмо практичний приклад. Припустімо, у нас є клас користувача, і ми хочемо під час серіалізації додати до об’єкта контрольну суму, щоб під час десеріалізації перевірити цілісність даних.
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// transient поле — його не серіалізуємо
private transient int checksum;
public User(String name, int age) {
this.name = name;
this.age = age;
this.checksum = calculateChecksum();
}
private int calculateChecksum() {
return (name != null ? name.hashCode() : 0) + age;
}
// Власна серіалізація
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // Зберігаємо стандартні поля
int sum = calculateChecksum();
out.writeInt(sum); // Записуємо контрольну суму
System.out.println("[LOG] Серіалізація User: checksum=" + sum);
}
// Власна десеріалізація
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Відновлюємо стандартні поля
int sum = in.readInt(); // Читаємо контрольну суму
int actual = calculateChecksum();
System.out.println("[LOG] Десеріалізація User: checksum=" + sum + ", actual=" + actual);
if (sum != actual) {
throw new IOException("Дані пошкоджено! Контрольна сума не збігається.");
}
this.checksum = actual;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", checksum=" + checksum + "}";
}
}
Приклад використання:
// Зберігаємо об’єкт
User user = new User("Alice", 42);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(user);
}
// Завантажуємо об’єкт
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User loaded = (User) in.readObject();
System.out.println("Відновлений об’єкт: " + loaded);
}
Що відбувається?
- Під час серіалізації викликається writeObject, зберігаються стандартні поля + контрольна сума.
- Під час десеріалізації викликається readObject, відновлюються поля + перевіряється контрольна сума.
- У консолі з’явиться лог, і якщо щось не так — буде кинуто виняток.
3. Вилучення чутливих даних із серіалізації
Іноді потрібно, щоб певні поля не серіалізувалися (наприклад, паролі). Для цього можна використовувати ключове слово transient (про нього докладніше — у наступній лекції), але можна й вручну не серіалізувати поле, якщо ви реалізуєте writeObject.
Приклад:
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient — не серіалізується
// Але можна зробити і так:
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// Не записуємо password!
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// password залишається null
}
}
Увага:
Якщо ви хочете серіалізувати лише частину об’єкта — просто не записуйте зайві поля в потік.
4. Виклик методів суперкласу: defaultWriteObject і defaultReadObject
Усередині ваших методів writeObject і readObject майже завжди потрібно викликати defaultWriteObject() і defaultReadObject(). Це як натиснути «зберегти чернетку» перед тим, як додати власні нотатки.
Ці методи відповідають за стандартну серіалізацію всіх не-transient, не-static полів поточного класу та суперкласу. Якщо їх не викликати, ці поля не будуть серіалізовані й під час десеріалізації виявляться порожніми.
Приклад неправильної поведінки:
private void writeObject(ObjectOutputStream out) throws IOException {
// out.defaultWriteObject(); // забули викликати!
out.writeInt(123); // щось своє
}
У цьому випадку стандартні поля просто не збережуться!
5. Практика: логування процесу серіалізації
Додаймо логування до нашого класу користувача, щоб бачити, коли відбуваються серіалізація та десеріалізація.
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("[LOG] Серіалізація Person: " + name + ", вік " + age);
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
System.out.println("[LOG] Десеріалізація Person: " + name + ", вік " + age);
}
}
Використання:
Person p = new Person("Bob", 30);
// Зберігаємо у файл
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
out.writeObject(p);
}
// Завантажуємо з файлу
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"))) {
Person loaded = (Person) in.readObject();
}
Результат:
У консолі ви побачите повідомлення про те, що об’єкт серіалізується та десеріалізується.
6. Типові помилки при використанні writeObject/readObject
Помилка № 1: Не викликано defaultWriteObject/defaultReadObject. Якщо забути викликати ці методи, стандартні поля не будуть серіалізовані, і об’єкт після десеріалізації виявиться порожнім або некоректним.
Помилка № 2: Неправильна сигнатура методів. Методи мають бути суворо private void writeObject(ObjectOutputStream) і private void readObject(ObjectInputStream). Якщо зробити їх public/protected або змінити параметри — вони не будуть викликані автоматично.
Помилка № 3: Виняток у методі. Якщо в writeObject або readObject станеться виняток, серіалізація або десеріалізація перерветься, і об’єкт не буде коректно збережений/завантажений.
Помилка № 4: Пропущена серіалізація/десеріалізація суперкласу. Якщо ваш клас успадковується від іншого серіалізованого класу, обов’язково викликайте defaultWriteObject/defaultReadObject, інакше поля суперкласу не збережуться.
Помилка № 5: Серіалізація чутливих даних. Якщо ви забули виключити паролі або інші приватні дані, вони потраплять у серіалізований файл. Використовуйте transient або не серіалізуйте їх вручну.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ