CodeGym /Kursy /JAVA 25 SELF /pola transient, serialVersionUID

pola transient, serialVersionUID

JAVA 25 SELF
Poziom 43 , Lekcja 1
Dostępny

1. Więcej o transient

W Javie słowo kluczowe transient — to sposób, by powiedzieć serializatorowi: „Proszę, nie dotykaj tego pola, zapomnij o nim przy zapisywaniu obiektu!”. Jeśli oznaczysz pole jako transient, nie trafi ono do zserializowanego strumienia bajtów. Jest to szczególnie przydatne dla danych wrażliwych (np. haseł) lub obliczeń tymczasowych, których nie trzeba zapisywać.

Przykład: po co jest transient?

Załóżmy, że mamy klasę użytkownika:

import java.io.Serializable;

public class User implements Serializable {
    private String username;
    private transient String password; // Nie chcemy zapisywać hasła!

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // Tutaj mamy gettery i settery
}

Jeśli zserializujemy obiekt tej klasy, pole password nie trafi do pliku (ani innego strumienia). Oznacza to, że po deserializacji hasło przyjmie wartość domyślną — dla obiektów to null, dla liczb — 0, dla booleanfalse.

Jak to działa w praktyce?

Zróbmy mały eksperyment. Najpierw zserializujemy użytkownika:

import java.io.*;

public class TransientDemo {
    public static void main(String[] args) throws Exception {
        User user = new User("john", "qwerty123");

        // Zapisujemy obiekt do pliku
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.ser"));
        out.writeObject(user);
        out.close();

        // Teraz odczytujemy obiekt z powrotem
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.ser"));
        User restored = (User) in.readObject();
        in.close();

        System.out.println("Username: " + restored.username);
        System.out.println("Password: " + restored.password);
    }
}

Wynik:

Username: john
Password: null

Jak widać, pole password nie zostało odtworzone — jest transient, więc serializator je zignorował.

Gdzie i po co używać transient?

  • Hasła i tokeny. Nigdy ich nie serializuj!
  • Dane cache’owane lub tymczasowe. Na przykład pole, które można obliczyć „w locie”.
  • Obiekty, których nie da się lub nie trzeba serializować. Np. referencje do połączeń z bazą danych, strumienie, gniazda.

Specyfika zachowania pól transient

Gdy obiekt jest deserializowany, wszystkie pola oznaczone jako transient otrzymują wartości domyślne. Jeśli trzeba przywrócić im sens, można skorzystać z metody readObject i uzupełnić je ręcznie (przeliczyć cache, poprosić użytkownika o hasło itp.).

2. serialVersionUID: unikalny identyfikator wersji klasy

serialVersionUID — to specjalne statyczne pole typu long, które określa „wersję” serializowalnej klasy. Podczas serializacji zapisywana jest wartość serialVersionUID, a podczas deserializacji JVM porównuje ją z wartością w bieżącej klasie. Jeśli się nie zgadzają — zostanie rzucony wyjątek i obiekt nie zostanie odtworzony.

Jak zadeklarować serialVersionUID?

Bardzo prosto:

private static final long serialVersionUID = 1L;

Zwykle deklaruje się je bezpośrednio w klasie implementującej Serializable:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // ... pozostałe pola i metody
}

Po co jest serialVersionUID?

Wyobraź sobie, że zapisałeś obiekt klasy do pliku, a potem zmieniłeś strukturę klasy (dodałeś pole, coś przemianowałeś itd.). Jeśli serialVersionUID się różni, JVM uzna, że klasa jest niezgodna ze starą wersją i nie pozwoli deserializować obiektu. To zapobiega nieoczekiwanym błędom.

Co się stanie, jeśli nie zadeklarować serialVersionUID?

Jeśli nie zadeklarujesz serialVersionUID jawnie, kompilator wygeneruje go sam — na podstawie struktury klasy. Jednak nawet drobna zmiana (np. dodanie lub usunięcie pola) spowoduje zmianę serialVersionUID. W efekcie nie będzie można deserializować obiektów zapisanych starą wersją klasy.

Dlatego zaleca się zawsze jawnie ustawiać serialVersionUID!

Demonstracja: niedopasowanie serialVersionUID

1) Najpierw utworzymy klasę i zserializujemy obiekt:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;

    public User(String username) {
        this.username = username;
    }
}

2) Następnie zmienimy serialVersionUID:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 2L; // Było 1L, stało się 2L!
    private String username;

    public User(String username) {
        this.username = username;
    }
}

Wynik:

java.io.InvalidClassException: User; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

JVM uczciwie ostrzega: „Wersje są niekompatybilne!”

Jaką wartość serialVersionUID wybrać?

Najczęściej używa się prostych wartości (1L, 2L, 42L), a w dużych projektach IDE generuje „długie” wartości. Najważniejsze — zmieniać je tylko wtedy, gdy struktura klasy zmienia się w sposób niekompatybilny.

3. Praktyka: transient i serialVersionUID w działaniu

Przykład: klasa z polem transient

Zmodyfikujmy aplikację szkoleniową (np. menedżer kontaktów) i dodajmy do klasy użytkownika pole do przechowywania tymczasowego tokena autoryzacji, który nie powinien trafiać do serializacji.

import java.io.Serializable;

public class Contact implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private String phone;
    private transient String sessionToken; // tymczasowy token

    public Contact(String name, String phone, String sessionToken) {
        this.name = name;
        this.phone = phone;
        this.sessionToken = sessionToken;
    }

    @Override
    public String toString() {
        return "Contact{" +
               "name='" + name + '\'' +
               ", phone='" + phone + '\'' +
               ", sessionToken='" + sessionToken + '\'' +
               '}';
    }
}

Teraz spróbujmy zserializować i zdeserializować obiekt:

import java.io.*;

public class TransientAndSUIDDemo {
    public static void main(String[] args) throws Exception {
        Contact c = new Contact("John", "+19990001122", "token-12345");

        // Zapisujemy obiekt
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("contact.ser"));
        out.writeObject(c);
        out.close();

        // Odtwarzamy obiekt
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("contact.ser"));
        Contact restored = (Contact) in.readObject();
        in.close();

        System.out.println("Przed serializacją: " + c);
        System.out.println("Po deserializacji: " + restored);
    }
}

Wyjście:

Przed serializacją: Contact{name='John', phone='+19990001122', sessionToken='token-12345'}
Po deserializacji: Contact{name='John', phone='+19990001122', sessionToken='null'}

Jak widać, pole sessionToken nie zostało odtworzone — jest transient.

Przykład: eksperyment z serialVersionUID

1) Najpierw serializujemy obiekt z serialVersionUID = 1L.
2) Potem zmieniamy serialVersionUID na 2L i próbujemy zdeserializować ten sam plik.

Wynik: otrzymasz InvalidClassException, tak jak pokazano powyżej.

4. Dlaczego lepiej jawnie ustawiać serialVersionUID?

  • Jawne — lepsze od niejawnego. Kontrolujesz kompatybilność: jeśli struktura klasy nie zmieniła się krytycznie, zostawiasz stare serialVersionUID, a obiekty spokojnie się deserializują.
  • Automatyczne generowanie jest niebezpieczne. Każda zmiana może zmienić wyliczoną wartość i „zepsuć” kompatybilność zapisanych danych.
  • IDE pomoże. Większość IDE (np. IntelliJ IDEA) potrafi automatycznie generować serialVersionUID.

5. Typowe błędy przy pracy z transient i serialVersionUID

Błąd nr 1: zapomniano oznaczyć wrażliwe pole jako transient.
W efekcie hasła lub tokeny przypadkowo trafiają do zserializowanych plików. To nie tylko niezręczne, ale i niebezpieczne.

Błąd nr 2: nie zadeklarowano serialVersionUID jawnie.
Klasa została zmieniona i teraz nie da się deserializować starych obiektów: JVM uznaje je za niekompatybilne, choć w istocie struktura mogła nie zmienić się krytycznie.

Błąd nr 3: zmieniono serialVersionUID bez potrzeby.
Jeśli tylko dodałeś getter lub komentarz, nie ma potrzeby zmieniać serialVersionUID — w przeciwnym razie stare dane przestaną się deserializować.

Błąd nr 4: serialVersionUID nie jest static ani final.
Pole powinno być zadeklarowane jako private static final long serialVersionUID. W przeciwnym razie JVM nie rozpozna go poprawnie.

Błąd nr 5: pole transient zapomniano przywrócić po deserializacji.
Jeśli wartość jest krytyczna dla działania obiektu, odtwórz ją w readObject — w przeciwnym razie obiekt może działać niepoprawnie.

1
Zadanie
JAVA 25 SELF, poziom 43, lekcja 1
Niedostępne
Znacznik czasu dla danych osobowych: wersjonowanie encji
Znacznik czasu dla danych osobowych: wersjonowanie encji
1
Zadanie
JAVA 25 SELF, poziom 43, lekcja 1
Niedostępne
Sekrety Wynagrodzeń: Tymczasowe Zniknięcie i Przywrócenie do Domyślnych
Sekrety Wynagrodzeń: Tymczasowe Zniknięcie i Przywrócenie do Domyślnych
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION