CodeGym /Kursy /JAVA 25 SELF /Externalizable: precyzyjna kontrola serializacji

Externalizable: precyzyjna kontrola serializacji

JAVA 25 SELF
Poziom 43, Lekcja 2
Dostępny

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:

  1. Zaimplementować interfejs java.io.Externalizable.
  2. 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ć.

1
Zadanie
JAVA 25 SELF, poziom 43, lekcja 2
Niedostępne
Profil użytkownika: kontrolowane zapisywanie danych
Profil użytkownika: kontrolowane zapisywanie danych
1
Zadanie
JAVA 25 SELF, poziom 43, lekcja 2
Niedostępne
Współrzędne nawigacji: gra z kolejnością
Współrzędne nawigacji: gra z kolejnością
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION