1. NotSerializableException: gdy kolekcja nie chce się serializować
Najczęstszy i najbardziej podstępny błąd przy serializacji kolekcji — to java.io.NotSerializableException. Pojawia się, jeśli choć jeden element kolekcji nie implementuje interfejsu Serializable.
Spójrzmy na naiwny przykład:
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("Dombey i syn"));
// Próba zserializowania kolekcji
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
oos.writeObject(books); // BUM! NotSerializableException
}
}
}
Co się stanie? Na etapie oos.writeObject(books) otrzymasz wyjątek:
java.io.NotSerializableException: Book
Dlaczego? Ponieważ klasa Book nie implementuje interfejsu Serializable. Nawet jeśli sama kolekcja (ArrayList) potrafi się serializować, elementy kolekcji muszą być serializowalne!
Jak diagnozować
W błędzie zawsze wskazana jest klasa, która spowodowała problem — szukaj jej w komunikacie wyjątku. Jeśli kolekcja jest duża, a błąd pojawia się tylko w określonych warunkach, możliwe, że jakiś element został dodany przypadkowo i nie implementuje Serializable.
Jak naprawić
Dodaj implements Serializable do swojej klasy:
class Book implements Serializable {
String title;
Book(String title) { this.title = title; }
}
Wskazówka: Jeśli kolekcja zawiera różne typy obiektów, sprawdź wszystkie pod kątem zgodności z Serializable!
2. ClassCastException przy deserializacji: gdy generyki zawodzą
W Javie informacja o parametrach generycznych kolekcji jest wymazywana po kompilacji (type erasure). To oznacza, że jeśli zserializowałeś List<String>, a deserializujesz jako List<Integer>, kompilator nie zauważy błędu, ale w czasie wykonania otrzymasz ClassCastException.
Przykład:
// Serializacja
List<String> names = Arrays.asList("Anna", "Borys");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("names.ser"))) {
oos.writeObject(names);
}
// Deserializacja (NIEBEZPIECZNE!)
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("names.ser"))) {
List<Integer> numbers = (List<Integer>) ois.readObject(); // unchecked cast
Integer first = numbers.get(0); // BUM! ClassCastException
}
Błąd:
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
Jak uniknąć
- Nie używaj „surowych” kolekcji (raw types) i nie wykonuj rzutowań bez potrzeby.
- Sprawdzaj typy elementów po deserializacji, jeśli nie masz pewności co do ich zawartości.
- Dokumentuj, jaki typ kolekcji jest serializowany i jakiego oczekujesz przy odczycie.
Przykład bezpiecznej deserializacji:
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; // ostrzeżenie wyciszone, ale typ sprawdzony!
}
}
3. Zmiana struktury klas: serialVersionUID i zgodność wsteczna
Zserializowałeś kolekcję, a potem postanowiłeś dodać nowe pole do klasy elementu, zmienić nazwę pola albo w ogóle zmienić strukturę klasy. Teraz, próbując deserializować stary plik, otrzymasz zagadkowy błąd:
java.io.InvalidClassException: Book; local class incompatible: stream classdesc serialVersionUID = 1234, local class serialVersionUID = 5678
Dlaczego tak się dzieje
Każda serializowalna klasa otrzymuje unikalny identyfikator wersji — serialVersionUID. Jeśli klasa się zmieni (np. dodasz pole), JVM oblicza nowy serialVersionUID, a deserializacja wykrywa, że wersja klasy nie zgadza się z tą, która była przy serializacji.
Jak uniknąć
- Deklaruj jawnie serialVersionUID w swoich klasach:
class Book implements Serializable {
private static final long serialVersionUID = 1L;
String title;
// ...
}
- Zachowuj zgodność wsteczną: nie usuwaj ani nie zmieniaj nazw pól, jeśli planujesz czytać stare pliki.
- Testuj deserializację po zmianach.
Co zrobić, jeśli mimo wszystko trzeba zmienić klasę?
- Rozważ implementację metod readObject/writeObject do ręcznego sterowania serializacją.
- Albo wykonaj migrację danych: odczytaj stary plik za pomocą starej wersji klasy, a następnie zapisz ponownie w nowym formacie.
4. Utrata danych przy serializacji niezmiennych kolekcji
W nowych wersjach Javy pojawiły się niezmienne kolekcje, na przykład tworzone przez List.of(), Set.of(), Map.of(). W starszych wersjach Javy (przed 12) i niektórych zewnętrznych implementacjach serializacja takich kolekcji może działać niepoprawnie: po deserializacji kolekcja staje się zwykłą modyfikowalną albo w ogóle pojawia się błąd.
Przykład:
List<String> list = List.of("a", "b", "c");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
oos.writeObject(list);
}
W starszych JVM podczas deserializacji pojawiał się błąd albo kolekcja przestawała być niezmienna.
Jak uniknąć
- Sprawdzaj dokumentację używanej wersji Javy.
- Testuj serializację i deserializację takich kolekcji.
- Jeśli musisz zachować niezmienność, po deserializacji owiń kolekcję w Collections.unmodifiableList(list).
5. Serializacja pól transient i static
Co dzieje się z takimi polami:
- transient — pola oznaczone tym słowem kluczowym w ogóle nie są serializowane. Po deserializacji będą miały wartość domyślną (np. null lub 0).
- static — pola klasy (a nie obiektu) nigdy nie są serializowane.
Przykład:
class Book implements Serializable {
String title;
transient String cache; // nie jest serializowane!
static String publisher = "Default"; // również nie jest serializowane!
}
Dlaczego to ważne
Jeśli w obiekcie przechowujesz jakieś wartości wyliczane lub cache, oznacz je jako transient — to oszczędza miejsce i przyspiesza serializację.
Uwaga: Po deserializacji pola transient trzeba przeliczyć lub zainicjalizować na nowo.
6. Serializacja dużych kolekcji: wydajność i rozmiar pliku
Problemy:
- Duże kolekcje (np. milion obiektów) mogą prowadzić do ogromnych plików, długiego czasu zapisu i odczytu, a czasem nawet do braku pamięci (OutOfMemoryError).
- Przy serializacji grafu obiektów (np. złożonych, wzajemnie powiązanych kolekcji) rozmiar pliku może nieoczekiwanie wzrosnąć.
Jak uniknąć
- Serializuj kolekcję partiami: na przykład zapisuj obiekty pojedynczo albo w małych porcjach.
- Używaj przetwarzania strumieniowego: zamiast serializować całą kolekcję naraz, serializuj elementy w miarę potrzeby.
- Kompresuj pliki: używaj GZIPOutputStream, aby zmniejszyć rozmiar pliku.
Przykład serializacji strumieniowej:
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
for (Book book : bigList) {
oos.writeObject(book);
}
}
Uwaga: Przy takim podejściu deserializacja wymaga wiedzy, ile obiektów zapisano (albo użycia specjalnego „znacznika końca”).
GO TO FULL VERSION