CodeGym /Kursy /JAVA 25 SELF /Omówienie typowych błędów przy serializacji kolekcji

Omówienie typowych błędów przy serializacji kolekcji

JAVA 25 SELF
Poziom 44, Lekcja 4
Dostępny

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”).

1
Zadanie
JAVA 25 SELF, poziom 44, lekcja 4
Niedostępne
Błędna interpretacja danych: `ClassCastException` po deserializacji 🚨
Błędna interpretacja danych: `ClassCastException` po deserializacji 🚨
1
Zadanie
JAVA 25 SELF, poziom 44, lekcja 4
Niedostępne
Cyfrowy archiwista: efektywne przechowywanie dużych ilości danych ze kompresją 🗄️
Cyfrowy archiwista: efektywne przechowywanie dużych ilości danych ze kompresją 🗄️
1
Ankieta/quiz
Serializacja złożonych struktur, poziom 44, lekcja 4
Niedostępny
Serializacja złożonych struktur
Serializacja złożonych struktur
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION