CodeGym /Kursy /JAVA 25 SELF /Serializacja kolekcji generycznych: specyfika

Serializacja kolekcji generycznych: specyfika

JAVA 25 SELF
Poziom 45 , Lekcja 1
Dostępny

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.

1
Zadanie
JAVA 25 SELF, poziom 45, lekcja 1
Niedostępne
Tajemnicze pudełko: sprawdzenie zawartości po wyjęciu
Tajemnicze pudełko: sprawdzenie zawartości po wyjęciu
1
Zadanie
JAVA 25 SELF, poziom 45, lekcja 1
Niedostępne
Osobisty asystent zakupów: zapisywanie planu tygodniowego
Osobisty asystent zakupów: zapisywanie planu tygodniowego
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION