1. Вступ
У Java для серіалізації об’єктів найчастіше використовують інтерфейс Serializable. Він простий: достатньо реалізувати інтерфейс, і об’єкт можна записувати/читати за допомогою ObjectOutputStream/ObjectInputStream. Але іноді цього недостатньо:
- Потрібно повністю контролювати, які поля і як серіалізуються.
- Необхідно забезпечити сумісність між різними версіями класу.
- Важливо зменшити розмір серіалізованого файлу або пришвидшити процес.
Для таких випадків у Java є інтерфейс Externalizable — більш «ручний» і гнучкий спосіб серіалізації.
Коротко:
- Serializable — автоматична серіалізація: Java сама вирішує, що і як записувати.
- Externalizable — ручна серіалізація: ви самі визначаєте, що і як зберігати/відновлювати.
2. Контракт Externalizable: реалізуємо writeExternal і readExternal
Щоб використовувати Externalizable, потрібно:
- Реалізувати інтерфейс java.io.Externalizable.
- Обов’язково реалізувати два методи:
- void writeExternal(ObjectOutput out) throws IOException
- void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
Приклад:
import java.io.*;
public class User implements Externalizable {
private String name;
private int age;
// Обов’язковий публічний конструктор без параметрів!
public User() {}
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
Важливо: розробник сам вирішує, які саме поля будуть серіалізуватися і в якому порядку. Але при цьому є одна обов’язкова вимога: клас повинен мати public конструктор без параметрів. Якщо його не буде, то під час десеріалізації програма викине виняток InvalidClassException.
3. Коли використовувати Externalizable?
Використовуйте Externalizable, якщо:
- Потрібен повний контроль над форматом даних. Наприклад, хочете серіалізувати лише частину полів або серіалізувати їх в особливому порядку/форматі.
- Оптимізація продуктивності та розміру файлу. Стандартна серіалізація додає службову інформацію (метадані, назви класів, типи тощо). З Externalizable ви записуєте лише необхідні дані.
- Забезпечення зворотної сумісності. Якщо структура класу змінюється, можна вручну реалізувати логіку читання старих і нових версій даних.
- Серіалізація нестандартних об’єктів. Наприклад, якщо у вас є поля, які не можна серіалізувати стандартним способом (наприклад, transient, volatile або складні структури).
Коли НЕ варто використовувати?
- Якщо вам не потрібен повний контроль — використовуйте Serializable, це простіше й безпечніше.
- Якщо ви не впевнені, що зможете підтримувати сумісність формату даних під час змін класу.
4. Плюси та мінуси Externalizable відносно Serializable
Плюси:
- Повний контроль над серіалізацією. Ви самі вирішуєте, що і як писати/читати.
- Компактність. Немає зайвих метаданих — лише ваші дані.
- Швидкість. Менше даних — швидше запис/читання.
- Гнучкість. Можна реалізувати підтримку різних версій формату, додати стиснення, шифрування тощо.
Мінуси:
- Ручна реалізація — легко помилитися. Якщо переплутати порядок запису/читання, серіалізація «зламається» (буде помилка або некоректні дані).
- Немає автоматичної підтримки transient, serialVersionUID. Усе потрібно продумувати та реалізовувати вручну.
- Складніше підтримувати. Під час зміни структури класу потрібно не забути оновити методи серіалізації.
- Обов’язковий публічний конструктор без параметрів.
- Менше «магії» — більше відповідальності.
5. Приклади: серіалізація та десеріалізація простого об’єкта
Серіалізація об’єкта
User user = new User("Alice", 30);
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); // Alice (30)
}
Увага: якщо ви зміните порядок запису/читання полів або забудете серіалізувати якесь поле, дані будуть некоректними! Методи writeExternal і readExternal мають бути суворо узгоджені за послідовністю операцій.
Приклад: серіалізуємо лише частину полів
public class SecretUser implements Externalizable {
private String login;
private transient String password; // transient не має значення для Externalizable
public SecretUser() {}
public SecretUser(String login, String password) {
this.login = login;
this.password = password;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(login);
// Не серіалізуємо пароль!
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
login = in.readUTF();
password = null; // пароль не відновлюємо
}
}
6. Практика: порівняння розміру серіалізованого файлу
Порівняймо, скільки «важать» файли, серіалізовані через Serializable і через Externalizable.
Клас із Serializable
public class UserSerializable implements Serializable {
private String name;
private int age;
public UserSerializable(String name, int age) {
this.name = name;
this.age = age;
}
}
Клас із Externalizable
public class UserExternalizable implements Externalizable {
private String name;
private int age;
public UserExternalizable() {}
public UserExternalizable(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
}
}
Код для порівняння
import java.io.*;
public class CompareSerialization {
public static void main(String[] args) throws Exception {
UserSerializable s = new UserSerializable("Bob", 25);
UserExternalizable e = new UserExternalizable("Bob", 25);
// Serializable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
out.writeObject(s);
}
// Externalizable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ext.bin"))) {
out.writeObject(e);
}
System.out.println("Serializable file size: " + new File("ser.bin").length());
System.out.println("Externalizable file size: " + new File("ext.bin").length());
}
}
Результат:
Файл ser.bin (Serializable) зазвичай більший — містить службову інформацію Java. Файл ext.bin (Externalizable) — лише ваші дані, зазвичай менший.
7. Типові помилки під час роботи з Externalizable
Помилка № 1: Відсутній публічний конструктор без параметрів.
Клас, що реалізує Externalizable, обов’язково повинен мати public конструктор без аргументів. Без нього десеріалізація призведе до винятку InvalidClassException.
Помилка № 2: Порушення порядку запису та читання полів.
Методи writeExternal і readExternal мають працювати в одному й тому самому порядку. Якщо під час запису спочатку зберегти поле name, а під час читання спочатку спробувати зчитати age, дані будуть спотворені.
Помилка № 3: Пропущені поля під час серіалізації.
Якщо забути записати поле в writeExternal, під час десеріалізації воно матиме значення null (для посилальних типів) або 0 (для числових).
Помилка № 4: Неправильне використання transient або serialVersionUID.
На відміну від Serializable, для Externalizable ці механізми автоматично не працюють — ви маєте самі контролювати, які поля зберігати, а які ні.
Помилка № 5: Зміна структури класу без оновлення методів.
Якщо додати або видалити поля і не внести відповідні зміни в writeExternal і readExternal, старі збережені дані можуть перестати коректно завантажуватися.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ