1. Interfejs Serializable
Pamiętacie przykłady z poprzedniego wykładu? Klasy Javy, które chcemy serializować, muszą implementować specjalny interfejs — java.io.Serializable. To tzw. interfejs znacznikowy: nie zawiera żadnych metod, a po prostu „oznacza” klasę jako nadającą się do serializacji. Jeśli klasa implementuje ten interfejs, JVM pozwala serializować jej obiekty standardowymi środkami.
Nie warto serializować wszystkiego jak leci, ponieważ nie wszystkie obiekty można lub należy zapisywać do bajtów. Niektóre obiekty zależą od stanu systemu operacyjnego, otwartych plików lub połączeń sieciowych. Dlatego Java wymaga, aby wyraźnie oznaczyć klasę jako serializowalną.
Interfejs znacznikowy jest jak naklejka „dozwolone do pakowania” na pudełku. Jeśli takiej naklejki nie ma — pakowacz (JVM) odmawia pracy.
Przykład: deklaracja klasy serializowalnej
import java.io.Serializable;
public class User implements Serializable {
private String name;
private int age;
// Konstruktor, gettery i settery
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Dla estetyki: metoda toString()
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
Zwróćcie uwagę:
- Po prostu dodaliśmy implements Serializable do deklaracji klasy.
- Nie trzeba implementować żadnych metod (interfejs jest pusty).
- Wszystkie standardowe klasy Javy, które można serializować (np. ArrayList, HashMap, String), już implementują Serializable.
2. Jak uczynić własną klasę serializowalną
Zasada nr 1: po prostu dodaj implements Serializable
To wszystko, czego wymaga sama klasa. Ale są pewne niuanse!
Ważne: wszystkie obiekty zagnieżdżone również muszą być serializowalne.
Jeśli twoja klasa ma pola‑referencje do innych obiektów, one także muszą być serializowalne. Na przykład:
public class Profile implements Serializable {
private User user; // User musi być serializowalny!
private int level;
}
Jeśli choć jedno z pól nie jest serializowalne, przy próbie serializacji wystąpi wyjątek.
3. Przykład serializacji i deserializacji
Zobaczmy, jak zserializować i zdeserializować obiekt do pliku. Do tego służą klasy ObjectOutputStream i ObjectInputStream.
Przykład: serializacja obiektu User do pliku
import java.io.*;
public class SerializeDemo {
public static void main(String[] args) {
User user = new User("Alice", 30);
// Zapisujemy obiekt do pliku
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("Obiekt pomyślnie zserializowany do pliku user.ser");
} catch (IOException e) {
System.out.println("Błąd serializacji: " + e.getMessage());
}
}
}
Co się tutaj dzieje?
- Tworzymy obiekt User.
- Otwieramy strumień ObjectOutputStream, który zapisuje do pliku "user.ser".
- Wywołujemy writeObject(user). W tym momencie JVM przekształca obiekt w strumień bajtów i zapisuje go do pliku.
Przykład: deserializacja obiektu z pliku
import java.io.*;
public class DeserializeDemo {
public static void main(String[] args) {
// Odczytujemy obiekt z pliku
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User user = (User) ois.readObject();
System.out.println("Obiekt został pomyślnie odtworzony: " + user);
} catch (IOException | ClassNotFoundException e) {
System.out.println("Błąd deserializacji: " + e.getMessage());
}
}
}
Co się tutaj dzieje?
- Otwieramy strumień ObjectInputStream, czytający z pliku "user.ser".
- Wywołujemy readObject(). JVM odtwarza obiekt z bajtów.
- Nie zapomnij rzutować wynik do właściwego typu (User), ponieważ readObject() zwraca Object.
- Może wystąpić ClassNotFoundException, jeśli klasa User nie zostanie znaleziona podczas deserializacji.
Wszystko razem: serializacja i deserializacja
import java.io.*;
public class SerializationExample {
public static void main(String[] args) {
User user = new User("Bob", 22);
// Serializacja
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("Serializacja zakończona!");
} catch (IOException e) {
System.out.println("Błąd serializacji: " + e.getMessage());
}
// Deserializacja
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User loaded = (User) ois.readObject();
System.out.println("Deserializacja zakończona! " + loaded);
} catch (IOException | ClassNotFoundException e) {
System.out.println("Błąd deserializacji: " + e.getMessage());
}
}
}
Wynik:
Serializacja zakończona!
Deserializacja zakończona! User{name='Bob', age=22}
4. Co dzieje się „pod maską” podczas serializacji
Gdy wywołujesz writeObject, JVM najpierw sprawdza, czy klasa implementuje interfejs Serializable. Jeśli klasa nie jest oznaczona jako serializowalna, zostaje zgłoszony wyjątek. Następnie JVM przechodzi po wszystkich zwykłych polach obiektu (czyli tych, które nie są static ani transient) i zapisuje ich wartości do strumienia bajtów. Jeśli wśród tych pól znajdują się inne obiekty, serializacja stosowana jest do nich rekurencyjnie, ale tylko wtedy, gdy również implementują Serializable.
Podczas deserializacji obiekt tworzony jest bez wywołania zwykłego konstruktora, a jego pola są wypełniane zapisanymi wartościami — jakby „konstruktor bez konstruktora” ożywiał obiekt ze strumienia bajtów.
Niektóre pola nie będą serializowane. Pola statyczne (static) należą do samej klasy, a nie do pojedynczego obiektu, dlatego ich wartości nie są zapisywane. Pola oznaczone jako transient również są pomijane — to wygodne dla danych tymczasowych, pamięci podręcznej lub poufnych informacji, takich jak hasła.
Schemat procesu serializacji
flowchart TB
A[Obiekt User w pamięci] -- writeObject --> B[ObjectOutputStream]
B -- zapisuje bajty --> C[Plik user.ser]
C -- readObject --> D[ObjectInputStream]
D -- odtwarza --> E[Obiekt User w pamięci]
5. Typowe błędy podczas używania Serializable
Błąd nr 1: pole‑referencja do nieserializowalnego obiektu. Jeśli w klasie User pojawi się pole typu, na przykład, Thread lub Socket, serializacja nie zadziała. Nie wszystkie obiekty da się zserializować — pamiętajcie o tym!
Błąd nr 2: nieserializowalne klasy zagnieżdżone. Jeśli klasa User zawiera klasę wewnętrzną, która nie jest static, serializacja może się nie powieść. Lepiej używać zagnieżdżonych klas static lub osobnych klas w oddzielnych plikach.
Błąd nr 3: próba serializacji pola static. Pola statyczne nie są serializowane — należą do klasy, a nie do obiektu. Po deserializacji static-pole będzie miało wartość określoną w klasie, a nie w zserializowanym obiekcie.
Błąd nr 4: niezgodność wersji klasy. Jeśli zmienisz strukturę klasy po serializacji (na przykład dodasz lub usuniesz pole), a następnie spróbujesz zdeserializować stary obiekt, może pojawić się błąd InvalidClassException. Do kontroli wersji służy specjalne pole serialVersionUID — o nim opowiemy szerzej w kolejnym wykładzie.
GO TO FULL VERSION