1. Серіалізація та десеріалізація generics‑колекцій
Дженерики (узагальнення) в Java — це не магія, а радше ілюзія, яку підтримує компілятор. На етапі компіляції інформація про типи параметрів generics стирається (це називається type erasure, або «стирання типів»). Тобто під час виконання (runtime) колекція List<String> нічим не відрізняється від List<Object> чи List<Integer>. Усі вони — просто List, і JVM не знає, які саме типи там лежать.
Розгляньмо приклад:
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.getClass() == intList.getClass()); // true!
Тут усе влаштовано хитро. Ми створюємо два списки — один для рядків, інший для чисел. На рівні компілятора Java суворо стежить, щоб ви не поклали в List<String> щось, окрім рядків, а в List<Integer> — щось, окрім чисел. Але щойно програма запускається, відмінності зникають. Для JVM обидва об’єкти — просто ArrayList, і перевірити, які саме елементи там мають зберігатися, вона вже не може. Саме тому порівняння класів двох списків (stringList.getClass() == intList.getClass()) повертає true.
Звідси й важливий висновок: generics у Java потрібні насамперед для зручності та безпеки на етапі компіляції. Але під час виконання ці «мітки» губляться. Тому, якщо ви серіалізуєте колекцію, до файлу потраплять лише самі дані, а не інформація про generic‑типи. Тобто збережеться список значень, але за файлом не можна зрозуміти, що це був саме List<String>, а не List<Object> чи List<Integer>.
Ще один приклад: серіалізація та десеріалізація List<String>
import java.io.*;
import java.util.*;
public class GenericSerializationDemo {
public static void main(String[] args) throws Exception {
List<String> fruits = new ArrayList<>();
fruits.add("Яблуко");
fruits.add("Банан");
fruits.add("Апельсин");
// Серіалізація
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fruits.ser"))) {
oos.writeObject(fruits);
}
// Десеріалізація
List<String> loadedFruits;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fruits.ser"))) {
loadedFruits = (List<String>) ois.readObject();
}
System.out.println(loadedFruits); // [Яблуко, Банан, Апельсин]
}
}
Зверніть увагу на рядок:
loadedFruits = (List<String>) ois.readObject();
Тут ми явно приводимо результат до типу List<String>, хоча насправді під час виконання це просто ArrayList. Компілятор не зможе перевірити, що це справді список рядків, і якщо там раптом опиняться не рядки — ми отримаємо ClassCastException уже під час роботи програми.
2. Проблеми під час десеріалізації generics‑колекцій
Втрата інформації про тип елементів
Оскільки інформація про generic‑параметри стирається, після десеріалізації Java не може гарантувати, що в колекції лежать саме ті об’єкти, які ви очікуєте. Усе, що ви отримуєте, — це «сира» колекція (raw type); компілятор цього не перевіряє, тож проблема може проявитися вже під час виконання.
Демонстрація проблеми
List rawList = new ArrayList();
rawList.add("Кіт");
rawList.add(42); // Integer!
// Десеріалізація
List<String> loadedCats = (List<String>) ois.readObject();
String cat = loadedCats.get(1); // ClassCastException!
Попередження про unchecked cast
Компілятор чесно попередить про потенційну проблему:
Note: GenericSerializationDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
Це попередження означає, що ви приводите тип без перевірки, тож у колекції можуть опинитися об’єкти неочікуваного типу.
3. Особливості серіалізації generics‑колекцій
Немає інформації про generic‑параметри у файлі
Коли ви серіалізуєте List<String> і List<Integer>, у файлі не буде жодної інформації про те, що це були рядки чи числа. Вміст колекції серіалізується «як є» — об’єкти по порядку.
Якщо ви відкриєте серіалізований файл у текстовому редакторі, то не побачите там жодного слова про <String> або <Integer>. Усе це — лише на рівні вихідного коду й компілятора.
Приклад: серіалізація різних колекцій
List<Integer> numbers = Arrays.asList(1, 2, 3);
List<String> words = Arrays.asList("один", "два", "три");
// Серіалізація
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"))) {
oos.writeObject(numbers);
oos.writeObject(words);
}
У файлі test.ser — просто два об’єкти типу ArrayList, жодної інформації про generic‑параметри.
Проблема десеріалізації «сирих» колекцій
Якщо ви серіалізували список без generic‑параметра (raw type), а десеріалізуєте як List<String>, компілятор не зможе перевірити коректність типів, тож можливі помилки під час виконання.
4. Найкращі практики під час серіалізації generics‑колекцій
Документуйте очікувані типи елементів.
Якщо ваш API серіалізує колекцію, обов’язково вказуйте, який тип елементів очікується. Наприклад: «Цей метод повертає серіалізований List<User>».
Перевіряйте типи елементів після десеріалізації.
Після десеріалізації колекції корисно перевірити, що всі елементи мають очікуваний тип (особливо якщо джерело даних вам не підконтрольне).
for (Object obj : loadedList) {
if (!(obj instanceof String)) {
throw new IllegalStateException("Очікувався рядок, але знайдено: " + obj.getClass());
}
}
Використовуйте незмінні колекції.
Якщо ви серіалізуєте колекцію лише для читання, використовуйте незмінні колекції — List.copyOf, Collections.unmodifiableList. Це допоможе уникнути випадкової зміни даних після десеріалізації.
Не змішуйте типи в одній колекції.
Намагайтеся не серіалізувати колекції з елементами різних типів (наприклад, List<Object> із різними класами всередині). Це ускладнить десеріалізацію і може призвести до помилок.
Обережно використовуйте придушення попереджень.
Якщо ви впевнені, що десеріалізуєте колекцію з правильним типом елементів, можна придушити попередження компілятора за допомогою анотації @SuppressWarnings("unchecked"):
@SuppressWarnings("unchecked")
List<String> loaded = (List<String>) ois.readObject();
Але робіть це усвідомлено — легко приховати проблему аж до продакшену.
5. Приклад: серіалізація та десеріалізація колекції з власним класом
Припустімо, у нас є клас User:
import java.io.Serializable;
public class User implements Serializable {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
Серіалізуємо список користувачів:
List<User> users = Arrays.asList(
new User("Аліса", 30),
new User("Боб", 25)
);
// Серіалізація
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users.ser"))) {
oos.writeObject(users);
}
// Десеріалізація
List<User> loadedUsers;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users.ser"))) {
loadedUsers = (List<User>) ois.readObject();
}
System.out.println(loadedUsers); // [Аліса (30), Боб (25)]
Усе працює! Але якщо хтось підкине в серіалізований файл об’єкт іншого класу, ви можете отримати ClassCastException під час спроби читати елементи як User.
6. Серіалізація вкладених generics‑колекцій
Колекції можуть бути вкладеними, наприклад: List<List<String>>, Map<String, List<User>> тощо. Java серіалізує такі структури рекурсивно, але правила лишаються тими самими:
- Усі вкладені колекції й елементи мають бути серіалізовними.
- Інформація про generic‑параметри як і раніше стирається.
Приклад: серіалізація списку списків
List<List<String>> matrix = new ArrayList<>();
matrix.add(Arrays.asList("a", "b"));
matrix.add(Arrays.asList("c", "d"));
// Серіалізація
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("matrix.ser"))) {
oos.writeObject(matrix);
}
// Десеріалізація
List<List<String>> loadedMatrix;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("matrix.ser"))) {
loadedMatrix = (List<List<String>>) ois.readObject();
}
System.out.println(loadedMatrix); // [[a, b], [c, d]]
7. Корисні нюанси
Серіалізація generics‑колекцій із різними реалізаціями
Інколи ви серіалізуєте одну реалізацію колекції, а десеріалізуєте як іншу. Наприклад, серіалізували ArrayList, а десеріалізуєте як LinkedList. Це призведе до помилки приведення типів:
List<String> list = new ArrayList<>();
// ...
List<String> loaded = (LinkedList<String>) ois.readObject(); // ClassCastException!
Порада: Завжди десеріалізуйте в той самий тип, який серіалізували, або використовуйте інтерфейс (List), якщо вам не важлива конкретна реалізація.
Використання бібліотек (наприклад, Gson, Jackson)
Бібліотеки для JSON (наприклад, Gson, Jackson) вміють серіалізувати/десеріалізувати колекції з generics, але через стирання типів під час десеріалізації потребують явного зазначення типу. Приклад для Gson:
Type type = new com.google.gson.reflect.TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);
8. Generics і серіалізація в Map і Set
Усі правила вище справедливі й для інших колекцій із generics:
- Під час серіалізації Map<String, Integer> інформація про типи ключів і значень не зберігається.
- Під час десеріалізації доводиться приводити до потрібного типу й ретельно перевіряти вміст.
Приклад: серіалізація Map
Map<String, Integer> scores = new HashMap<>();
scores.put("Вася", 90);
scores.put("Петя", 85);
// Серіалізація
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("scores.ser"))) {
oos.writeObject(scores);
}
// Десеріалізація
Map<String, Integer> loadedScores;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("scores.ser"))) {
loadedScores = (Map<String, Integer>) ois.readObject();
}
System.out.println(loadedScores); // {Вася=90, Петя=85}
9. Типові помилки під час серіалізації generics‑колекцій
Помилка № 1: ClassCastException під час десеріалізації. Якщо ви десеріалізуєте колекцію як List<String>, а в ній опиниться об’єкт іншого типу, отримаєте ClassCastException під час виконання. Завжди перевіряйте вміст колекції!
Помилка № 2: NotSerializableException через не‑серіалізовний елемент. Якщо хоча б один елемент колекції не реалізує Serializable, серіалізація завершиться з помилкою NotSerializableException. Переконуйтеся в серіалізовності всіх класів, які можуть опинитися в колекції.
Помилка № 3: Втрата інформації про generic‑параметри. Після десеріалізації не покладайтеся на generic‑параметри — їх немає під час виконання. Використовуйте явні перевірки типів, якщо є сумніви в коректності даних.
Помилка № 4: Невідповідність реалізацій колекцій. Серіалізували ArrayList, а десеріалізуєте як LinkedList — отримаєте помилку приведення. Намагайтеся десеріалізувати в той самий тип, який було серіалізовано.
Помилка № 5: Несумісність версій класів. Якщо структура класу елемента колекції змінилася після серіалізації (наприклад, додали поле), можливі помилки під час десеріалізації. Використовуйте serialVersionUID для контролю версій.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ