1. Metody writeReplace i readResolve: teoria
Czasem standardowe mechanizmy serializacji nie wystarczają. Wyobraź sobie sytuację: masz singleton (klasę, której w całym programie może istnieć tylko jedna instancja) i chcesz, aby po deserializacji nadal pozostała jedyną instancją (a nie pojawił się nowy klon). Albo chcesz zserializować nie sam obiekt, lecz jego „odchudzoną” wersję (proxy), aby ukryć szczegóły implementacji lub oszczędzić miejsce.
W Javie istnieją do tego specjalne metody: writeReplace i readResolve. Ich zadaniem jest podmiana serializowanego lub deserializowanego obiektu na inny.
Prosta analogia:
To tak, jakbyś wysyłał paczkę do przyjaciela, ale zamiast siebie włożył do pudełka zabawkowego sobowtóra. A gdy przyjaciel rozpakowuje paczkę, zamiast zabawki lądujesz u niego w rękach ty — prawdziwy! (W prawdziwym życiu to tak nie działa, ale w Javie — jak najbardziej.)
writeReplace
Metoda private Object writeReplace() jest wywoływana na obiekcie przed serializacją. Może zwrócić dowolny obiekt, który będzie faktycznie serializowany zamiast pierwotnego. Jeśli nie jest zaimplementowana — serializowany jest sam obiekt.
Sygnatura:
private Object writeReplace() throws ObjectStreamException
readResolve
Metoda private Object readResolve() jest wywoływana na obiekcie po deserializacji. Pozwala zamienić właśnie utworzony obiekt na inny (np. zwrócić singleton lub zcache’owaną instancję).
Sygnatura:
private Object readResolve() throws ObjectStreamException
Ważne:
Obie metody muszą być private i zwracać Object. Wymaga tego specyfikacja serializacji Javy. Jeśli będą public — mechanizm serializacji je po prostu zignoruje.
2. Zastosowanie writeReplace i readResolve w praktyce
Singleton i readResolve
Singleton to po prostu klasa, której w całym programie może istnieć tylko jedna instancja. Jeśli taki obiekt zserializować, a potem odtworzyć, to bez metody readResolve pojawi się nowa instancja i zasada „jedyności” zostanie złamana. Z readResolve można zwrócić dokładnie ten sam obiekt, zachowując ideę singletonu.
import java.io.*;
public class MySingleton implements Serializable {
private static final MySingleton INSTANCE = new MySingleton();
private MySingleton() {}
public static MySingleton getInstance() {
return INSTANCE;
}
// Gwarantujemy, że po deserializacji zwrócony zostanie właśnie INSTANCE
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
Wyjaśnienie:
Bez readResolve po deserializacji pojawi się nowy obiekt, nierówny (według ==) oryginalnemu singletonowi. Z readResolve — zawsze zwracane jest INSTANCE.
Sprawdźmy w praktyce:
MySingleton s1 = MySingleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.bin"));
out.writeObject(s1);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.bin"));
MySingleton s2 = (MySingleton) in.readObject();
in.close();
System.out.println(s1 == s2); // true, jeśli jest readResolve; false — bez niego
writeReplace: serializacja obiektu proxy
Czasem obiekt jest zbyt „ciężki” do serializacji, zawiera wrażliwe dane albo po prostu nie powinien trafiać na zewnątrz w pełnej postaci. W takim przypadku można zserializować „zamiennik” — obiekt proxy.
Przykład:
Załóżmy, że mamy klasę User z prywatnym hasłem. Nie chcemy, aby hasło było serializowane.
import java.io.*;
public class User implements Serializable {
private String username;
private transient String password; // transient — nie jest serializowane
public User(String username, String password) {
this.username = username;
this.password = password;
}
// Zamiast User serializujemy jedynie UserProxy
private Object writeReplace() throws ObjectStreamException {
return new UserProxy(username);
}
// Klasa proxy — tylko do serializacji
private static class UserProxy implements Serializable {
private String username;
public UserProxy(String username) {
this.username = username;
}
private Object readResolve() throws ObjectStreamException {
// W prawdziwym życiu hasła nie da się odtworzyć — zwracamy User z pustym hasłem
return new User(username, "");
}
}
}
Wyjaśnienie:
- Przy serializacji User zamienia się w UserProxy (bez hasła).
- Przy deserializacji UserProxy zamienia się z powrotem w User (ale hasło jest już puste).
3. Dostosowanie serializacji dla obiektów niemutowalnych
Niemutowalne (niezmienne) obiekty często używają prywatnych pól final i nie mają setterów. Standardowa serializacja Javy potrafi obejść to ograniczenie, ale czasem lepiej jawnie kontrolować proces przez writeReplace/readResolve.
Przykład: Value Object
import java.io.*;
public final class Money implements Serializable {
private final int amount;
private final String currency;
public Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
private Object writeReplace() throws ObjectStreamException {
return new MoneyProxy(amount, currency);
}
private static class MoneyProxy implements Serializable {
private final int amount;
private final String currency;
MoneyProxy(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
private Object readResolve() throws ObjectStreamException {
return new Money(amount, currency);
}
}
}
Wyjaśnienie:
- Przy serializacji Money zamienia się w MoneyProxy (POJO).
- Przy deserializacji MoneyProxy zamienia się z powrotem w Money.
Współdziałanie z writeObject/readObject
Metody writeReplace/readResolve działają niezależnie od writeObject/readObject. Jeśli oba mechanizmy są zdefiniowane, najpierw wywoływane jest writeReplace, a dopiero na zwróconym obiekcie — writeObject (jeśli implementuje Serializable).
Schemat:
flowchart LR
A[Obiekt] -- writeReplace --> B[Obiekt proxy]
B -- writeObject --> C[Strumień bajtów]
C -- readObject --> D[Obiekt proxy]
D -- readResolve --> E[Obiekt wynikowy]
4. Praktyka: serializacja z podmianą obiektu
Dodajmy niestandardową serializację w twojej aplikacji ćwiczeniowej — na przykład dla klasy Person, tak aby podczas serializacji zapisywane było tylko imię, a wiek ignorowano (załóżmy, że dbamy o prywatność).
Krok 1. Klasa główna
import java.io.*;
public class Person implements Serializable {
private String name;
private int age; // nie chcemy serializować
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private Object writeReplace() throws ObjectStreamException {
return new PersonProxy(name);
}
private static class PersonProxy implements Serializable {
private final String name;
PersonProxy(String name) {
this.name = name;
}
private Object readResolve() throws ObjectStreamException {
return new Person(name, -1); // -1 — "wiek nieznany"
}
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
Krok 2. Testujemy
public class TestCustomSerialization {
public static void main(String[] args) throws Exception {
Person original = new Person("Alice", 30);
// Serializacja
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"));
out.writeObject(original);
out.close();
// Deserializacja
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"));
Person deserialized = (Person) in.readObject();
in.close();
System.out.println("Przed serializacją: " + original);
System.out.println("Po deserializacji: " + deserialized);
}
}
Wynik:
Przed serializacją: Person{name='Alice', age=30}
Po deserializacji: Person{name='Alice', age=-1}
Jak widać, wiek nie został zserializowany — wszystko zgodnie z planem!
5. Szczegóły i niuanse
Kiedy używać writeReplace/readResolve?
- Gdy trzeba serializować tylko część stanu obiektu.
- Do serializacji/deserializacji obiektów proxy.
- Do wsparcia wzorca Singleton.
- Dla obiektów niemutowalnych lub złożonych, których wewnętrzna struktura może się zmieniać.
Kiedy nie warto używać?
- Jeśli można obejść się polami transient lub writeObject/readObject.
- Jeśli obiekt nie powinien być podmieniany na inny.
Zgodność z dziedziczeniem
Jeśli nadklasa definiuje writeReplace/readResolve, zostaną one wywołane także dla podklas (jeśli nie zostaną przesłonięte). Zachowaj ostrożność przy hierarchiach!
6. Typowe błędy przy niestandardowej serializacji
Błąd nr 1: Nieprawidłowa widoczność metod. Jeśli writeReplace/readResolve nie są private, mechanizm serializacji ich nie wywoła. Tylko private!
Błąd nr 2: Niezgodny typ zwracany. writeReplace/readResolve muszą zwracać Object. Nawet jeśli faktycznie zwracasz swój typ — metodę zadeklaruj z typem zwracanym Object.
Błąd nr 3: Utrata danych. Jeśli obiekt proxy nie zawiera wszystkich potrzebnych danych do odtworzenia obiektu źródłowego, część informacji zostanie utracona. Zawsze upewnij się, że będziesz w stanie odtworzyć obiekt z powrotem.
Błąd nr 4: Naruszenie inwariantów. readResolve powinien zwracać obiekt, który odpowiada oczekiwaniom programu (np. dla singletona — właśnie INSTANCE).
Błąd nr 5: Nieobsłużone wyjątki. writeReplace/readResolve mogą zgłaszać ObjectStreamException. Obsłuż je lub jawnie przekaż dalej.
GO TO FULL VERSION