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, старые сохранённые данные могут перестать корректно загружаться.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ