1. Po co jest potrzebna serializacja
Wyobraź sobie, że twój obiekt — to rzeczy, które zabierasz na wakacje. Serializacja — to zapakowanie całej zawartości walizki do specjalnego kontenera, który można włożyć do bagażu albo wysłać pocztą. Deserializacja, odpowiednio, — to rozpakowanie tego kontenera i uzyskanie rzeczy w pierwotnej postaci.
W istocie serializacja zamienia obiekt w strumień bajtów, który można zapisać do pliku, przesłać przez sieć lub po prostu przechowywać w pamięci. Deserializacja robi odwrotnie: odtwarza obiekt z tego strumienia. Upraszczając do maksimum, serializacja — to jak „zamrożenie” obiektu, aby potem go „rozmrozić” i otrzymać z powrotem w tym samym stanie.
Zachowywanie stanu obiektów między uruchomieniami programu
Jednym z najczęstszych scenariuszy jest zachowywanie stanu programu. Na przykład masz listę użytkowników, wyniki gry albo ustawienia aplikacji. To wszystko wygodnie przechowywać bezpośrednio jako obiekty. Aby dane nie ginęły między uruchomieniami, serializuje się je do pliku, a przy następnym uruchomieniu — deserializuje.
Dobry przykład — zwykły zapis gry. Gdy gracz przechodzi poziom, jego postęp jest „zamrażany” i zapisywany do pliku przy użyciu serializacji. Następnego dnia uruchamia grę i postęp jest „rozmrażany”: dane z pliku znów stają się obiektami i gracz kontynuuje od miejsca, w którym skończył.
Stwórzmy taki prosty zapis:
import java.io.*;
// Klasa gracza musi być Serializable
class Player implements Serializable {
String name;
int score;
Player(String name, int score) {
this.name = name;
this.score = score;
}
}
public class GameSaveExample {
public static void main(String[] args) throws Exception {
// Tworzymy obiekt gracza
Player player = new Player("Ihor", 1500);
// --- Zapis (serializacja) ---
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("save.dat"))) {
out.writeObject(player);
System.out.println("Postęp zapisany!");
}
// --- Wczytywanie (deserializacja) ---
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("save.dat"))) {
Player loaded = (Player) in.readObject();
System.out.println("Postęp wczytany: " + loaded.name + " z punktami " + loaded.score);
}
}
}
Zwróć uwagę: aby ten kod działał, klasa Player musi implementować interfejs Serializable. Więcej o nim — w następnej lekcji!
- Player — zwykła klasa z polami imię i punkty, oznaczona interfejsem Serializable (implements Serializable).
- ObjectOutputStream zapisuje obiekt do pliku "save.dat".
- ObjectInputStream odczytuje ten sam obiekt z powrotem.
- W efekcie mamy prawdziwy zapis: przy następnym uruchomieniu program wczyta obiekt gracza z tym samym stanem.
Przesyłanie obiektów przez sieć i między JVM
W systemach rozproszonych często trzeba przesyłać obiekty między różnymi programami, a nawet różnymi maszynami. Na przykład masz klienta i serwer, które muszą wymieniać się wiadomościami. Serializacja pozwala „zapakować” obiekt po jednej stronie, wysłać go przez sieć i „rozpakować” po drugiej stronie.
Przykład: Klient wysyła serwerowi obiekt zamówienia (Order), serwer go odbiera, deserializuje i przetwarza.
Zastosowania w technologiach Java
- RMI (Remote Method Invocation): umożliwia wywoływanie metod zdalnych obiektów — serializacja jest potrzebna do przekazywania argumentów i wartości zwracanych.
- Sesje HTTP: w serwletach obiekty w sesji są serializowane przy ponownym uruchomieniu kontenera.
- JMS (Java Message Service): wiadomości między komponentami mogą być serializowane.
- Keszowanie: obiekty mogą być serializowane w celu przechowywania w pamięci podręcznej (na dysku lub w rozproszonym magazynie).
Keszowanie i przenośność
Jeśli chcesz szybko zapisywać wyniki pośrednie (np. do keszowania), serializacja — to świetne narzędzie. Serializujesz obiekt, zapisujesz go na dysku lub w pamięci, a potem szybko odtwarzasz bez ponownych obliczeń.
2. Przykłady scenariuszy użycia serializacji
Zapis kolekcji użytkowników do pliku
Załóżmy, że masz klasę User:
public class User {
String name;
int age;
// ... inne pola
}
I masz listę użytkowników:
List<User> users = new ArrayList<>();
users.add(new User("Vasya", 25));
users.add(new User("Masha", 30));
// ... i tak dalej
Aby zapisać tę listę do pliku, serializujesz ją. Gdy będzie potrzebna — deserializujesz i otrzymujesz tę samą listę z tymi samymi użytkownikami. Przypominamy, że klasa User (i wszystkie jej pola) musi wspierać serializację, czyli implementować Serializable.
Przesyłanie wiadomości między klientem a serwerem
Klasyczny przykład — czat. Użytkownik pisze wiadomość, obiekt Message jest serializowany i wysyłany przez sieć. Serwer otrzymuje strumień bajtów, deserializuje obiekt, przetwarza go i, być może, przekazuje dalej.
import java.io.*;
import java.net.*;
// Wiadomość musi być Serializable
class Message implements Serializable {
String text;
Message(String text) {
this.text = text;
}
}
// Serwer
class Server {
public static void main(String[] args) throws Exception {
try (ServerSocket serverSocket = new ServerSocket(5000)) {
System.out.println("Serwer czeka na połączenie...");
Socket socket = serverSocket.accept();
System.out.println("Klient się połączył!");
try (ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
Message msg = (Message) in.readObject();
System.out.println("Odebrano wiadomość: " + msg.text);
}
}
}
}
// Klient
class Client {
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket("localhost", 5000)) {
try (ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream())) {
Message msg = new Message("Cześć, serwer!");
out.writeObject(msg);
System.out.println("Wiadomość wysłana!");
}
}
}
}
Jak to działa:
- Najpierw uruchamiasz Server (on czeka na połączenie).
- Potem uruchamiasz Client (łączy się z "localhost:5000").
- Klient serializuje obiekt Message i wysyła go przez gniazdo.
- Serwer odbiera strumień bajtów, deserializuje go i wypisuje tekst.
Tutaj używamy gniazd (ServerSocket, Socket) — to mechanizm komunikacji sieciowej, którego będziesz się uczyć później. Ważne teraz nie są szczegóły działania sieci, lecz sama idea: klient tworzy obiekt Message, serializuje go i wysyła; serwer otrzymuje strumień bajtów, deserializuje z powrotem do obiektu i wypisuje wiadomość. Zatem nawet jeśli na razie nie jest jasne, czym są klasy ServerSocket i Socket, przykład pokazuje wartość serializacji: dzięki niej można „zapakować” obiekt, przesłać go przez sieć, a po drugiej stronie rozpakować bez zbędnych przekształceń.
Keszowanie obiektów
W dużych aplikacjach często stosuje się keszowanie w celu przyspieszenia działania. Na przykład wyniki złożonych obliczeń są serializowane i zapisywane w pamięci podręcznej (plik, baza danych, rozproszony magazyn). Przy następnym żądaniu wynik można szybko odtworzyć, deserializując obiekt.
import java.io.*;
// Wynik obliczeń, który chcemy przechowywać w pamięci podręcznej
class Result implements Serializable {
int value;
Result(int value) {
this.value = value;
}
}
public class CacheExample {
private static final String CACHE_FILE = "cache.dat";
public static void main(String[] args) throws Exception {
Result result;
// Sprawdzamy, czy istnieje pamięć podręczna
File file = new File(CACHE_FILE);
if (file.exists()) {
// Wczytujemy wynik z pamięci podręcznej
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) {
result = (Result) in.readObject();
System.out.println("Wczytano z pamięci podręcznej: " + result.value);
}
} else {
// "Ciężkie" obliczenie (dla przykładu po prostu kwadrat liczby)
int x = 12345;
System.out.println("Liczymy... (to potrwa)");
result = new Result(x * x);
// Zapisujemy wynik do pamięci podręcznej
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) {
out.writeObject(result);
System.out.println("Zapisano do pamięci podręcznej: " + result.value);
}
}
}
}
3. Ograniczenia i ryzyka serializacji
Serializacja — to potężne narzędzie, ale nie bez pułapek. Omówmy główne ograniczenia i ryzyka.
Nie wszystkie obiekty można zserializować
W Javie nie wszystkie obiekty mogą być serializowane „z pudełka”. Na przykład obiekty powiązane z zasobami zewnętrznymi (pliki, połączenia sieciowe, strumienie wejścia/wyjścia) nie podlegają serializacji. To logiczne: nie da się zserializować „otwartego pliku” czy „żywego” połączenia sieciowego — ich stan zależy od systemu operacyjnego i środowiska wykonawczego.
Przykład: Klasy z polem typu FileInputStream nie da się zserializować — próba serializacji spowoduje błąd.
Kwestie bezpieczeństwa
Serializacja — to potencjalna luka w bezpieczeństwie. Jeśli deserializujesz dane pochodzące z niepewnego źródła (na przykład z internetu), napastnik może podsunąć złośliwy strumień bajtów, który doprowadzi do nieoczekiwanego zachowania twojego programu, a czasem — nawet do wykonania złośliwego kodu.
Zasada: Nigdy nie deserializuj danych z niezaufanych źródeł! To jak przyjmowanie paczki od nieznanego nadawcy — w środku może być cokolwiek.
Zgodność wersji
Jeśli zmienisz strukturę klasy (na przykład dodasz lub usuniesz pole), wcześniej zserializowane obiekty mogą stać się niezgodne z nową wersją klasy. To może prowadzić do błędów przy deserializacji. Szczegóły omówimy w kolejnych wykładach.
Wydajność
Binarna serializacja w Javie jest dość szybka, ale czasem niezbyt kompaktowa i nie zawsze wygodna do wymiany z innymi językami programowania. Do integracji z systemami zewnętrznymi często używa się formatów tekstowych (JSON, XML).
4. Typowe błędy przy pierwszym kontakcie z serializacją
Błąd nr 1: próba serializacji obiektu, który nie implementuje interfejsu Serializable.
W rezultacie otrzymasz wyjątek NotSerializableException. Pamiętaj, aby jawnie dodać implements Serializable w klasie i dopilnować, by wszystkie pola również były serializowalne!
Błąd nr 2: serializacja obiektów z nieserializowalnymi polami.
Jeśli twoja klasa zawiera pole typu, który nie wspiera serializacji (na przykład strumień lub połączenie z bazą danych), serializacja się nie powiedzie. Rozwiązanie — oznaczyć takie pola jako transient (o tym później).
Błąd nr 3: deserializacja danych z niepewnych źródeł.
To może prowadzić do podatności bezpieczeństwa, a nawet do wykonania złośliwego kodu. Ufaj tylko tym danym, które zostały zserializowane przez twój program!
Błąd nr 4: zmiany struktury klasy po serializacji.
Jeśli zapisałeś obiekt, a potem dodałeś lub usunąłeś pole w klasie, przy próbie deserializacji wystąpi błąd lub pojawią się „dziwne” wartości. Więcej — w następnych wykładach.
GO TO FULL VERSION