CodeGym /Kursy /JAVA 25 SELF /Kontrola nad procesem serializacji: writeObject, readObje...

Kontrola nad procesem serializacji: writeObject, readObject

JAVA 25 SELF
Poziom 43 , Lekcja 0
Dostępny

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.

1
Zadanie
JAVA 25 SELF, poziom 43, lekcja 0
Niedostępne
Encja Magicznego Kota: Zapisywanie i Budzenie z Rejestrowaniem
Encja Magicznego Kota: Zapisywanie i Budzenie z Rejestrowaniem
1
Zadanie
JAVA 25 SELF, poziom 43, lekcja 0
Niedostępne
Profil Gracza: Zachowanie Istoty, Zapomnienie Szczegółów
Profil Gracza: Zachowanie Istoty, Zapomnienie Szczegółów
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION