1. NotSerializableException: когда коллекция не хочет сериализоваться
Самая частая и самая коварная ошибка при сериализации коллекций — это java.io.NotSerializableException. Она возникает, если хотя бы один элемент коллекции не реализует интерфейс Serializable.
Давайте посмотрим на наивный пример:
import java.io.*;
import java.util.*;
class Book {
String title;
Book(String title) { this.title = title; }
}
public class LibraryApp {
public static void main(String[] args) throws Exception {
List<Book> books = new ArrayList<>();
books.add(new Book("Домби и сын"));
// Попытка сериализовать коллекцию
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
oos.writeObject(books); // БУМ! NotSerializableException
}
}
}
Что произойдёт? На этапе oos.writeObject(books) вы получите исключение:
java.io.NotSerializableException: Book
Почему? Потому что класс Book не реализует интерфейс Serializable. Даже если сама коллекция (ArrayList) умеет сериализоваться, элементы коллекции должны быть сериализуемыми!
Как диагностировать
В ошибке всегда указан класс, который стал причиной проблемы — ищите его в сообщении об исключении. Если коллекция большая, а ошибка возникает только при определённых условиях, возможно, какой-то элемент добавился случайно и не реализует Serializable.
Как исправить
Добавьте implements Serializable к вашему классу:
class Book implements Serializable {
String title;
Book(String title) { this.title = title; }
}
Совет: Если коллекция содержит разные типы объектов, проверьте их все на соответствие Serializable!
2. ClassCastException при десериализации: когда дженерики подводят
В Java информация о generic-параметрах коллекций стирается после компиляции (type erasure). Это значит, что если вы сериализовали List<String>, а десериализуете как List<Integer>, компилятор не заметит ошибки, но на этапе исполнения вы получите ClassCastException.
Пример:
// Сериализация
List<String> names = Arrays.asList("Анна", "Борис");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("names.ser"))) {
oos.writeObject(names);
}
// Десериализация (ОПАСНО!)
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("names.ser"))) {
List<Integer> numbers = (List<Integer>) ois.readObject(); // unchecked cast
Integer first = numbers.get(0); // БУМ! ClassCastException
}
Ошибка:
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
Как избежать
- Не используйте «сырые» коллекции (raw types) и не приводите типы без необходимости.
- Проверяйте типы элементов после десериализации, если не уверены в их содержимом.
- Документируйте, какой тип коллекции сериализуется и ожидается при чтении.
Пример безопасной десериализации:
Object obj = ois.readObject();
if (obj instanceof List<?>) {
List<?> list = (List<?>) obj;
if (!list.isEmpty() && list.get(0) instanceof String) {
@SuppressWarnings("unchecked")
List<String> safeNames = (List<String>) obj; // warning подавлен, но тип проверен!
}
}
3. Изменение структуры классов: serialVersionUID и backward compatibility
Вы сериализовали коллекцию, а потом решили добавить новое поле в класс элемента, изменить имя поля или вообще поменять структуру класса. Теперь при попытке десериализовать старый файл получите загадочную ошибку:
java.io.InvalidClassException: Book; local class incompatible: stream classdesc serialVersionUID = 1234, local class serialVersionUID = 5678
Почему это происходит
Каждый сериализуемый класс получает уникальный идентификатор версии — serialVersionUID. Если класс изменился (например, вы добавили поле), JVM вычисляет новый serialVersionUID, и десериализация видит, что версия класса не совпадает с той, что была при сериализации.
Как избежать
- Объявляйте явно serialVersionUID в своих классах:
class Book implements Serializable {
private static final long serialVersionUID = 1L;
String title;
// ...
}
- Сохраняйте обратную совместимость: не удаляйте и не переименовывайте поля, если планируете читать старые файлы.
- Тестируйте десериализацию после изменений.
Что делать, если всё-таки нужно изменить класс?
- Рассмотрите реализацию методов readObject/writeObject для ручного управления сериализацией.
- Или мигрируйте данные: прочитайте старый файл через старую версию класса, затем пересохраните в новом формате.
4. Потеря данных при сериализации неизменяемых коллекций
В новых версиях Java появились неизменяемые коллекции, например, созданные через List.of(), Set.of(), Map.of(). В старых версиях Java (до 12) и некоторых сторонних реализациях сериализация таких коллекций может не работать корректно: после десериализации коллекция становится обычной изменяемой или вообще возникает ошибка.
Пример:
List<String> list = List.of("a", "b", "c");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
oos.writeObject(list);
}
В старых JVM при десериализации возникала ошибка или коллекция переставала быть неизменяемой.
Как избежать
- Проверяйте документацию по используемой версии Java.
- Тестируйте сериализацию и десериализацию таких коллекций.
- Если нужно сохранить неизменяемость, после десериализации оборачивайте коллекцию в Collections.unmodifiableList(list).
5. Сериализация transient и static полей
Что происходит с такими полями:
- transient — поля, помеченные этим ключевым словом, не сериализуются вообще. После десериализации они будут иметь значение по умолчанию (например, null или 0).
- static — поля класса (а не объекта) не сериализуются никогда.
Пример:
class Book implements Serializable {
String title;
transient String cache; // не сериализуется!
static String publisher = "Default"; // тоже не сериализуется!
}
Почему это важно
Если вы храните какие-то вычисляемые значения или кэш внутри объекта, помечайте их как transient — это экономит место и ускоряет сериализацию.
Осторожно: После десериализации transient-поля нужно пересчитать или инициализировать заново.
6. Сериализация больших коллекций: производительность и размер файла
Проблемы:
- Большие коллекции (например, миллион объектов) могут привести к огромным файлам, долгому времени записи и чтения, а иногда даже к нехватке памяти (OutOfMemoryError).
- При сериализации графа объектов (например, сложных взаимосвязанных коллекций) размер файла может неожиданно вырасти.
Как избежать
- Сериализуйте коллекцию по частям: например, записывайте объекты по одному или небольшими порциями.
- Используйте потоковую обработку: вместо сериализации всей коллекции сразу, сериализуйте элементы по мере необходимости.
- Сжимайте файлы: используйте GZIPOutputStream для уменьшения размера файла.
Пример потоковой сериализации:
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
for (Book book : bigList) {
oos.writeObject(book);
}
}
Внимание: При таком подходе десериализация требует знать, сколько объектов записано (или использовать специальный «маркер конца»).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ