1. Wprowadzenie
Automatyczna serializacja — to jak autopilot w samolocie: działa świetnie, dopóki wszystko idzie zgodnie z planem. Gdy pojawiają się szczególne warunki, okazuje się, że prosty mechanizm już nie wystarcza. Wyobraź sobie, że trzeba zapisać obiekt, ale nie wszystkie jego pola: jedne dane są tymczasowe, inne zbyt wrażliwe, by umieszczać je w pliku. Albo odwrotnie — przy zapisie trzeba dodać coś własnego: na przykład wersję lub sumę kontrolną. Zdarza się też, że przed zapisem lub odczytem danych trzeba wykonać walidację lub przekształcenie. A czasem zadanie jest jeszcze trudniejsze: zapewnić zgodność z poprzednimi wersjami klasy, jeśli jej struktura z czasem się zmieniła.
W takich sytuacjach staje się jasne: sama standardowa serializacja nie wystarczy. Trzeba wziąć sterowanie we własne ręce.
Specjalne metody serializacji: writeObject i readObject
W Javie istnieją dwie specjalne metody, które pozwalają w pełni kontrolować proces serializacji i deserializacji obiektu:
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
Ważne!
- Metody muszą być dokładnie private (nie public, nie protected, nie package-private).
- Sygnatury muszą dokładnie odpowiadać tym podanym powyżej.
- Jeśli te metody są zadeklarowane w Twojej klasie, zostaną wywołane zamiast standardowej serializacji/deserializacji.
Jak to działa?
Gdy wywołujesz ObjectOutputStream.writeObject(obj), JVM najpierw szuka w klasie obj metody private void writeObject(ObjectOutputStream). Jeśli istnieje — to właśnie ona zostanie wywołana. Analogicznie, przy deserializacji wywoływana jest private void readObject(ObjectInputStream).
Jeśli metody nie są zadeklarowane, używana jest standardowa serializacja.
Jak są zbudowane writeObject i readObject
Sygnatury metod
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
Wewnątrz tych metod musisz wywołać:
- out.defaultWriteObject(); — do serializacji standardowych (nie transient) pól superklasy i bieżącej klasy.
- in.defaultReadObject(); — do deserializacji standardowych pól.
Jeśli nie wywołasz tych metod, standardowe pola nie zostaną zserializowane — a po deserializacji obiekt będzie „pusty”. To jak zapomnieć włożyć paszport do walizki: formalnie dojedziesz, ale nie będziesz w stanie udowodnić, kim jesteś.
2. Przykład: dodajemy sumę kontrolną podczas serializacji
Przyjrzyjmy się praktycznemu przykładowi. Załóżmy, że mamy klasę użytkownika i chcemy podczas serializacji dodać do obiektu sumę kontrolną, aby przy deserializacji sprawdzić integralność danych.
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// pole transient — nie serializujemy go
private transient int checksum;
public User(String name, int age) {
this.name = name;
this.age = age;
this.checksum = calculateChecksum();
}
private int calculateChecksum() {
return (name != null ? name.hashCode() : 0) + age;
}
// Niestandardowa serializacja
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // Zapisujemy standardowe pola
int sum = calculateChecksum();
out.writeInt(sum); // Zapisujemy sumę kontrolną
System.out.println("[LOG] Serializacja User: checksum=" + sum);
}
// Niestandardowa deserializacja
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Odtwarzamy standardowe pola
int sum = in.readInt(); // Odczytujemy sumę kontrolną
int actual = calculateChecksum();
System.out.println("[LOG] Deserializacja User: checksum=" + sum + ", actual=" + actual);
if (sum != actual) {
throw new IOException("Dane uszkodzone! Suma kontrolna się nie zgadza.");
}
this.checksum = actual;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", checksum=" + checksum + "}";
}
}
Przykład użycia:
// Zapisujemy obiekt
User user = new User("Alice", 42);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(user);
}
// Wczytujemy obiekt
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User loaded = (User) in.readObject();
System.out.println("Odtworzony obiekt: " + loaded);
}
Co się dzieje?
- Podczas serializacji wywoływana jest writeObject, zapisywane są standardowe pola + suma kontrolna.
- Podczas deserializacji wywoływana jest readObject, pola są odtwarzane + weryfikowana jest suma kontrolna.
- W konsoli pojawi się log, a jeśli coś będzie nie tak — zostanie rzucony wyjątek.
3. Wykluczanie wrażliwych danych z serializacji
Czasem trzeba, aby określone pola nie były serializowane (np. hasła). Do tego można użyć słowa kluczowego transient (więcej o nim — w następnej lekcji), ale można też ręcznie pominąć pole w serializacji, jeśli implementujesz writeObject.
Przykład:
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient — nie jest serializowane
// Ale można zrobić i tak:
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// Nie zapisujemy password!
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// password pozostaje null
}
}
Uwaga:
Jeśli chcesz serializować tylko część obiektu — po prostu nie zapisuj zbędnych pól do strumienia.
4. Wywołanie metod nadklasy: defaultWriteObject i defaultReadObject
Wewnątrz Twoich metod writeObject i readObject prawie zawsze należy wywołać defaultWriteObject() i defaultReadObject(). To jak wcisnąć „zapisz szkic” zanim dodasz własne notatki.
Te metody odpowiadają za standardową serializację wszystkich nie-transient, nie-static pól bieżącej klasy i nadklasy. Jeśli ich nie wywołasz, pola te nie zostaną zserializowane i po deserializacji będą puste.
Przykład nieprawidłowego zachowania:
private void writeObject(ObjectOutputStream out) throws IOException {
// out.defaultWriteObject(); // zapomnieliśmy wywołać!
out.writeInt(123); // coś własnego
}
W takim przypadku standardowe pola po prostu nie zostaną zapisane!
5. Praktyka: logowanie procesu serializacji
Dodajmy logowanie do naszej klasy użytkownika, aby widzieć, kiedy zachodzą serializacja i deserializacja.
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("[LOG] Serializacja Person: " + name + ", wiek " + age);
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
System.out.println("[LOG] Deserializacja Person: " + name + ", wiek " + age);
}
}
Użycie:
Person p = new Person("Bob", 30);
// Zapis do pliku
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
out.writeObject(p);
}
// Odczyt z pliku
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"))) {
Person loaded = (Person) in.readObject();
}
Rezultat:
W konsoli zobaczysz komunikaty o tym, że obiekt jest serializowany i deserializowany.
6. Typowe błędy przy użyciu writeObject/readObject
Błąd nr 1: Nie wywołano defaultWriteObject/defaultReadObject. Jeśli zapomnisz wywołać te metody, standardowe pola nie zostaną zserializowane i obiekt po deserializacji będzie pusty lub niepoprawny.
Błąd nr 2: Nieprawidłowa sygnatura metod. Metody muszą być ściśle private void writeObject(ObjectOutputStream) oraz private void readObject(ObjectInputStream). Jeśli zrobisz je public/protected albo zmienisz parametry — nie zostaną wywołane automatycznie.
Błąd nr 3: Wyjątek w metodzie. Jeśli w writeObject lub readObject wystąpi wyjątek, serializacja lub deserializacja zostanie przerwana, a obiekt nie zostanie poprawnie zapisany/odczytany.
Błąd nr 4: Zapomniana serializacja/deserializacja nadklasy. Jeśli Twoja klasa dziedziczy po innej serializowalnej klasie, koniecznie wywołuj defaultWriteObject/defaultReadObject, w przeciwnym razie pola nadklasy nie zostaną zapisane.
Błąd nr 5: Serializacja wrażliwych danych. Jeśli zapomnisz wykluczyć hasła lub inne prywatne dane, trafią one do zserializowanego pliku. Użyj transient albo nie serializuj ich ręcznie.
GO TO FULL VERSION