1. Serializacja i deserializacja kolekcji generycznych
Generyki (typy ogólne) w Javie — to nie magia, lecz raczej iluzja podtrzymywana przez kompilator. Na etapie kompilacji informacja o typach parametrów generyków jest wymazywana (to się nazywa type erasure, czyli „wymazywanie typów”). Czyli w czasie wykonywania (runtime) kolekcja List<String> niczym się nie różni od List<Object> czy List<Integer>. Wszystkie to po prostu List, a JVM nie wie, jakie dokładnie typy tam leżą.
Przyjrzyjmy się przykładowi:
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.getClass() == intList.getClass()); // true!
Działa to dość podstępnie. Tworzymy dwie listy — jedną dla napisów, drugą dla liczb. Na etapie kompilacji Java pilnuje, żebyś nie włożył do List<String> niczego poza napisami, a do List<Integer> — niczego poza liczbami. Ale gdy tylko program się uruchomi, różnice znikają. Dla JVM oba obiekty to po prostu ArrayList i nie da się już sprawdzić, jakie elementy powinny być w środku. Właśnie dlatego porównanie klas dwóch list (stringList.getClass() == intList.getClass()) zwraca true.
Stąd ważny wniosek: generyki w Javie służą przede wszystkim wygodzie i bezpieczeństwu na etapie kompilacji. W runtime te „etykiety” się gubią. Dlatego jeśli serializujesz kolekcję, do pliku trafią tylko same dane, a nie informacja o typach generycznych. Zapisana zostanie lista wartości, ale z pliku nie da się wywnioskować, że to był akurat List<String>, a nie List<Object> czy List<Integer>.
Jeszcze jeden przykład: serializacja i deserializacja 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("Jabłko");
fruits.add("Banan");
fruits.add("Pomarańcza");
// Serializacja
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fruits.ser"))) {
oos.writeObject(fruits);
}
// Deserializacja
List<String> loadedFruits;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fruits.ser"))) {
loadedFruits = (List<String>) ois.readObject();
}
System.out.println(loadedFruits); // [Jabłko, Banan, Pomarańcza]
}
}
Zwróć uwagę na wiersz:
loadedFruits = (List<String>) ois.readObject();
Tutaj jawnie rzutujemy wynik na typ List<String>, chociaż w runtime to po prostu ArrayList. Kompilator nie będzie w stanie sprawdzić, czy to rzeczywiście lista napisów i jeśli trafią się tam inne obiekty — dostaniemy ClassCastException już w trakcie działania programu.
2. Problemy przy deserializacji kolekcji generycznych
Utrata informacji o typie elementów
Ponieważ informacja o parametrach generycznych jest wymazywana, po deserializacji Java nie może zagwarantować, że w kolekcji są dokładnie te obiekty, których oczekujesz. Otrzymujesz „surową” kolekcję (raw type) i kompilator nie zgłasza błędu, ale problem może ujawnić się w runtime.
Demonstracja problemu
List rawList = new ArrayList();
rawList.add("Kot");
rawList.add(42); // Integer!
// Deserializacja
List<String> loadedCats = (List<String>) ois.readObject();
String cat = loadedCats.get(1); // ClassCastException!
Unchecked cast warning
Kompilator uczciwie ostrzeże o potencjalnym problemie:
Note: GenericSerializationDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
To ostrzeżenie mówi, że rzutujesz typ bez weryfikacji i w kolekcji mogą być obiekty nieoczekiwanego typu.
3. Specyfika serializacji kolekcji generycznych
Brak informacji o parametrach generycznych w pliku
Gdy serializujesz List<String> i List<Integer>, w pliku nie będzie żadnej informacji o tym, że to były napisy lub liczby. Zawartość kolekcji jest serializowana „jak jest” — obiekty po kolei.
Jeśli otworzysz zserializowany plik w edytorze tekstu, nie zobaczysz tam ani słowa o <String> czy <Integer>. To wszystko istnieje tylko na poziomie kodu źródłowego i kompilatora.
Przykład: serializacja różnych kolekcji
List<Integer> numbers = Arrays.asList(1, 2, 3);
List<String> words = Arrays.asList("jeden", "dwa", "trzy");
// Serializacja
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"))) {
oos.writeObject(numbers);
oos.writeObject(words);
}
W pliku test.ser — po prostu dwa obiekty typu ArrayList, żadnych informacji o parametrach generycznych.
Problem deserializacji „surowych” kolekcji
Jeśli zserializowałeś listę bez parametru generycznego (raw type), a deserializujesz jako List<String>, kompilator nie będzie w stanie sprawdzić poprawności typów i możliwe są błędy w runtime.
4. Dobre praktyki przy serializacji kolekcji generycznych
Dokumentuj oczekiwane typy elementów.
Jeśli twój interfejs API serializuje kolekcję, koniecznie podaj, jakiego typu elementów oczekujesz. Na przykład: „Ta metoda zwraca zserializowany List<User>”.
Sprawdzaj typy elementów po deserializacji.
Po deserializacji kolekcji warto sprawdzić, czy wszystkie elementy mają oczekiwany typ (zwłaszcza jeśli źródło danych nie jest pod twoją kontrolą).
for (Object obj : loadedList) {
if (!(obj instanceof String)) {
throw new IllegalStateException("Oczekiwano String, ale znaleziono: " + obj.getClass());
}
}
Używaj niezmiennych kolekcji.
Jeśli serializujesz kolekcję tylko do odczytu, użyj niezmiennych kolekcji — List.copyOf, Collections.unmodifiableList. Pomoże to uniknąć przypadkowej modyfikacji danych po deserializacji.
Nie mieszaj typów w jednej kolekcji.
Unikaj serializowania kolekcji z elementami różnych typów (np. List<Object> z różnymi klasami w środku). Utrudni to deserializację i może prowadzić do błędów.
Ostrożnie używaj wyciszania ostrzeżeń.
Jeśli masz pewność, że deserializujesz kolekcję z właściwym typem elementów, możesz wyciszyć ostrzeżenie kompilatora adnotacją @SuppressWarnings("unchecked"):
@SuppressWarnings("unchecked")
List<String> loaded = (List<String>) ois.readObject();
Ale używaj tego świadomie — łatwo ukryć problem aż do produkcji.
5. Przykład: serializacja i deserializacja kolekcji z własną klasą
Załóżmy, że mamy klasę 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 + ")";
}
}
Serializujemy listę użytkowników:
List<User> users = Arrays.asList(
new User("Alice", 30),
new User("Bob", 25)
);
// Serializacja
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users.ser"))) {
oos.writeObject(users);
}
// Deserializacja
List<User> loadedUsers;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users.ser"))) {
loadedUsers = (List<User>) ois.readObject();
}
System.out.println(loadedUsers); // [Alice (30), Bob (25)]
Wszystko działa! Ale jeśli ktoś podmieni w zserializowanym pliku obiekt innej klasy, możesz dostać ClassCastException przy próbie odczytu elementów jako User.
6. Serializacja zagnieżdżonych kolekcji generycznych
Kolekcje mogą być zagnieżdżone, na przykład: List<List<String>>, Map<String, List<User>> itd. Java serializuje takie struktury rekurencyjnie, ale zasady pozostają te same:
- Wszystkie zagnieżdżone kolekcje i elementy muszą być serializowalne.
- Informacje o parametrach generycznych nadal są wymazywane.
Przykład: serializacja listy list
List<List<String>> matrix = new ArrayList<>();
matrix.add(Arrays.asList("a", "b"));
matrix.add(Arrays.asList("c", "d"));
// Serializacja
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("matrix.ser"))) {
oos.writeObject(matrix);
}
// Deserializacja
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. Przydatne niuanse
Serializacja kolekcji generycznych z różnymi implementacjami
Czasem serializujesz jedną implementację kolekcji, a deserializujesz jako inną. Na przykład, zserializowano ArrayList, a deserializujesz jako LinkedList. To doprowadzi do błędu rzutowania:
List<String> list = new ArrayList<>();
// ...
List<String> loaded = (LinkedList<String>) ois.readObject(); // ClassCastException!
Wskazówka: Zawsze deserializuj do tego samego typu, który serializowałeś, albo używaj interfejsu (List), jeśli nie zależy ci na konkretnej implementacji.
Użycie bibliotek (np. Gson, Jackson)
Biblioteki do JSON (np. Gson, Jackson) potrafią serializować/deserializować kolekcje z generykami, ale wymagają jawnego wskazania typu przy deserializacji z powodu wymazywania typów. Przykład dla Gson:
Type type = new com.google.gson.reflect.TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);
8. Generyki i serializacja w Map i Set
Wszystkie powyższe zasady obowiązują także dla innych kolekcji z generykami:
- Przy serializacji Map<String, Integer> informacja o typach kluczy i wartości nie jest zachowywana.
- Przy deserializacji trzeba rzutować na potrzebny typ i uważnie sprawdzić zawartość.
Przykład: serializacja Map
Map<String, Integer> scores = new HashMap<>();
scores.put("Name1", 90);
scores.put("Name2", 85);
// Serializacja
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("scores.ser"))) {
oos.writeObject(scores);
}
// Deserializacja
Map<String, Integer> loadedScores;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("scores.ser"))) {
loadedScores = (Map<String, Integer>) ois.readObject();
}
System.out.println(loadedScores); // {Name1=90, Name2=85}
9. Typowe błędy przy serializacji kolekcji generycznych
Błąd nr 1: ClassCastException podczas deserializacji. Jeśli deserializujesz kolekcję jako List<String>, a znajdzie się w niej obiekt innego typu, dostaniesz ClassCastException w runtime. Zawsze sprawdzaj zawartość kolekcji!
Błąd nr 2: NotSerializableException z powodu nieserializowalnego elementu. Jeśli choć jeden element kolekcji nie implementuje Serializable, serializacja zakończy się błędem NotSerializableException. Sprawdzaj serializowalność wszystkich klas, które mogą trafić do kolekcji.
Błąd nr 3: Utrata informacji o parametrach generycznych. Po deserializacji nie polegaj na parametrach generycznych — w runtime ich nie ma. Używaj jawnych sprawdzeń typów, jeśli masz wątpliwości co do poprawności danych.
Błąd nr 4: Niedopasowanie implementacji kolekcji. Zserializowałeś ArrayList, a deserializujesz jako LinkedList — dostaniesz błąd rzutowania. Staraj się deserializować do tego samego typu, który był serializowany.
Błąd nr 5: Niezgodność wersji klas. Jeśli struktura klasy elementu kolekcji zmieniła się po serializacji (np. dodano pole), możliwe są błędy przy deserializacji. Używaj serialVersionUID do kontroli wersji.
GO TO FULL VERSION