JavaRush /Курсы /JAVA 25 SELF /Разбор типичных ошибок при сериализации коллекций

Разбор типичных ошибок при сериализации коллекций

JAVA 25 SELF
44 уровень , 4 лекция
Открыта

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);
    }
}

Внимание: При таком подходе десериализация требует знать, сколько объектов записано (или использовать специальный «маркер конца»).

1
Задача
JAVA 25 SELF, 44 уровень, 4 лекция
Недоступна
Неверное толкование данных: ClassCastException после десериализации 🚨
Неверное толкование данных: ClassCastException после десериализации 🚨
1
Задача
JAVA 25 SELF, 44 уровень, 4 лекция
Недоступна
Цифровой архивариус: эффективное хранение больших объемов данных с сжатием 🗄️
Цифровой архивариус: эффективное хранение больших объемов данных с сжатием 🗄️
1
Опрос
Сериализация сложных структур, 44 уровень, 4 лекция
Недоступен
Сериализация сложных структур
Сериализация сложных структур
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
kasnil Уровень 49
10 февраля 2026
JEP 394: Pattern Matching for instanceof В 16 версии уже можно вместо:

if (obj instanceof List<?>) {
    List<?> list = (List<?>) obj;
    if (!list.isEmpty() && list.get(0) instanceof String) {
        @SuppressWarnings("unchecked")
        List<String> safeNames = (List<String>) obj;
    }
}
писать:

if (obj instanceof List<?> list && !list.isEmpty() && list.get(0) instanceof String) {
    @SuppressWarnings("unchecked")
    List<String> safeNames = (List<String>) obj;
}