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);
}
}
Увага: За такого підходу під час десеріалізації потрібно знати, скільки об’єктів записано (або використовувати спеціальний «маркер кінця»).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ