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 boolean — false.
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.
GO TO FULL VERSION