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 інформація про параметри дженеріків колекцій стирається після компіляції (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; // попередження придушено, але тип перевірено!
    }
}

3. Зміна структури класів: serialVersionUID і зворотна сумісність

Ви серіалізували колекцію, а потім вирішили додати нове поле в клас елемента, змінити ім’я поля або взагалі поміняти структуру класу. Тепер під час спроби десеріалізувати старий файл ви отримаєте незрозумілу помилку:

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
Опитування
Серіалізація складних структур, рівень 44, лекція 4
Недоступний
Серіалізація складних структур
Серіалізація складних структур
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ