1. Wprowadzenie
W Javie do serializacji obiektów najczęściej używa się interfejsu Serializable. Jest prosty: wystarczy zaimplementować interfejs i obiekt można zapisywać/odczytywać za pomocą ObjectOutputStream/ObjectInputStream. Jednak czasem to nie wystarcza:
- Trzeba mieć pełną kontrolę nad tym, które pola i w jaki sposób są serializowane.
- Należy zapewnić kompatybilność między różnymi wersjami klasy.
- Ważne jest zmniejszenie rozmiaru zserializowanego pliku lub przyspieszenie procesu.
Dla takich przypadków w Javie istnieje interfejs Externalizable — bardziej „ręczny” i elastyczny sposób serializacji.
W skrócie:
- Serializable — automatyczna serializacja: Java sama decyduje, co i jak zapisywać.
- Externalizable — ręczna serializacja: samodzielnie wskazujesz, co i jak zapisywać/odtwarzać.
2. Kontrakt Externalizable: implementujemy writeExternal i readExternal
Aby użyć Externalizable, należy:
- Zaimplementować interfejs java.io.Externalizable.
- Obowiązkowo zaimplementować dwie metody:
- void writeExternal(ObjectOutput out) throws IOException
- void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
Przykład:
import java.io.*;
public class User implements Externalizable {
private String name;
private int age;
// Obowiązkowy publiczny konstruktor bez parametrów!
public User() {}
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
Ważne: programista sam decyduje, które dokładnie pola będą serializowane i w jakiej kolejności. Jest jednak jeden obowiązkowy wymóg: klasa musi mieć public konstruktor bez parametrów. Jeśli go zabraknie, podczas deserializacji program zgłosi wyjątek InvalidClassException.
3. Kiedy używać Externalizable?
Używaj Externalizable, gdy:
- Potrzebujesz pełnej kontroli nad formatem danych. Na przykład chcesz serializować tylko część pól lub robić to w szczególnej kolejności/formacie.
- Optymalizacja wydajności i rozmiaru pliku. Standardowa serializacja dodaje informacje pomocnicze (metadane, nazwy klas, typy itp.). Z Externalizable zapisujesz tylko potrzebne dane.
- Zapewnienie kompatybilności wstecznej. Jeśli struktura klasy się zmienia, można ręcznie zaimplementować logikę odczytu starszych i nowszych wersji danych.
- Serializacja niestandardowych obiektów. Na przykład gdy masz pola, których nie da się serializować standardowo (np. transient, volatile albo złożone struktury).
Kiedy NIE warto używać?
- Jeśli nie potrzebujesz pełnej kontroli — użyj Serializable, to prostsze i bezpieczniejsze.
- Jeśli nie masz pewności, że zdołasz utrzymać kompatybilność formatu danych przy zmianach klasy.
4. Zalety i wady Externalizable względem Serializable
Zalety:
- Pełna kontrola nad serializacją. Sam decydujesz, co i jak zapisywać/odczytywać.
- Kompaktowość. Brak zbędnych metadanych — tylko twoje dane.
- Szybkość. Mniej danych — szybszy zapis/odczyt.
- Elastyczność. Można zaimplementować obsługę różnych wersji formatu, dodać kompresję, szyfrowanie itd.
Wady:
- Ręczna implementacja — łatwo o błąd. Jeśli pomylisz kolejność zapisu/odczytu, serializacja „się zepsuje” (pojawi się błąd albo dane będą nieprawidłowe).
- Brak automatycznego wsparcia transient, serialVersionUID. Wszystko trzeba przemyśleć i zaimplementować ręcznie.
- Trudniejsza konserwacja. Przy zmianie struktury klasy nie można zapomnieć o aktualizacji metod serializacji.
- Wymagany publiczny konstruktor bez parametrów.
- Mniej „magii” — więcej odpowiedzialności.
5. Przykłady: serializacja i deserializacja prostego obiektu
Serializacja obiektu
User user = new User("Alice", 30);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(user);
}
Deserializacja obiektu
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User loaded = (User) in.readObject();
System.out.println(loaded); // Alice (30)
}
Uwaga: jeśli zmienisz kolejność zapisu/odczytu pól lub zapomnisz zserializować jakieś pole, dane będą niepoprawne! Metody writeExternal i readExternal muszą być ściśle zgodne pod względem sekwencji operacji.
Przykład: serializujemy tylko część pól
public class SecretUser implements Externalizable {
private String login;
private transient String password; // transient nie ma znaczenia dla Externalizable
public SecretUser() {}
public SecretUser(String login, String password) {
this.login = login;
this.password = password;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(login);
// Nie serializujemy hasła!
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
login = in.readUTF();
password = null; // nie odtwarzamy hasła
}
}
6. Praktyka: porównanie rozmiaru zserializowanego pliku
Porównajmy, ile „ważą” pliki serializowane przez Serializable i przez Externalizable.
Klasa z Serializable
public class UserSerializable implements Serializable {
private String name;
private int age;
public UserSerializable(String name, int age) {
this.name = name;
this.age = age;
}
}
Klasa z Externalizable
public class UserExternalizable implements Externalizable {
private String name;
private int age;
public UserExternalizable() {}
public UserExternalizable(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
}
}
Kod do porównania
import java.io.*;
public class CompareSerialization {
public static void main(String[] args) throws Exception {
UserSerializable s = new UserSerializable("Bob", 25);
UserExternalizable e = new UserExternalizable("Bob", 25);
// Serializable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
out.writeObject(s);
}
// Externalizable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ext.bin"))) {
out.writeObject(e);
}
System.out.println("Serializable file size: " + new File("ser.bin").length());
System.out.println("Externalizable file size: " + new File("ext.bin").length());
}
}
Wynik:
Plik ser.bin (Serializable) jest zwykle większy — zawiera informacje pomocnicze Javy. Plik ext.bin (Externalizable) — tylko twoje dane, zwykle mniejszy.
7. Typowe błędy podczas pracy z Externalizable
Błąd nr 1: Brak publicznego konstruktora bez parametrów.
Klasa implementująca Externalizable musi mieć public konstruktor bez argumentów. Bez niego deserializacja spowoduje InvalidClassException.
Błąd nr 2: Naruszenie kolejności zapisu i odczytu pól.
Metody writeExternal i readExternal muszą działać w tej samej kolejności. Jeśli przy zapisie najpierw zachowasz pole name, a przy odczycie najpierw spróbujesz wczytać age, dane zostaną zniekształcone.
Błąd nr 3: Pominięte pola przy serializacji.
Jeśli zapomnisz zapisać pole w writeExternal, przy deserializacji będzie miało wartość null (dla typów referencyjnych) lub 0 (dla liczbowych).
Błąd nr 4: Nieprawidłowe użycie transient lub serialVersionUID.
W przeciwieństwie do Serializable, dla Externalizable te mechanizmy nie działają automatycznie — samodzielnie kontrolujesz, które pola zapisywać, a których nie.
Błąd nr 5: Zmiana struktury klasy bez aktualizacji metod.
Jeśli dodasz lub usuniesz pola i nie wprowadzisz odpowiednich zmian w writeExternal i readExternal, stare zapisane dane mogą przestać poprawnie się ładować.
GO TO FULL VERSION