CodeGym /課程 /JAVA 25 SELF /transient 欄位、serialVersionUID

transient 欄位、serialVersionUID

JAVA 25 SELF
等級 43 , 課堂 1
開放

1. 更詳談 transient

在 Java 中,關鍵字 transient 是對序列化器說:「拜託,別碰這個欄位,保存物件時把它忽略!」。如果你把欄位宣告為 transient,它就不會進入序列化後的位元組串流。這對敏感資料(例如密碼)或不需要保存的暫時計算結果特別有用。

範例:為什麼需要 transient

假設我們有一個使用者類別:

import java.io.Serializable;

public class User implements Serializable {
    private String username;
    private transient String password; // 不希望保存密碼!

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

    // 這裡有 getters 與 setters
}

如果我們序列化該類別的物件,欄位 password 不會寫入檔案(或其他串流)。這表示在反序列化時,密碼會是預設值——對物件而言是 null,對數值是 0,對 booleanfalse

實務上怎麼運作?

做個小實驗。先把使用者序列化:

import java.io.*;

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

        // 將物件寫入檔案
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.ser"));
        out.writeObject(user);
        out.close();

        // 現在把物件讀回來
        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);
    }
}

結果:

Username: vasya
Password: null

可以看到,欄位 password 沒有被還原——它是 transient,因此序列化器忽略了它。

在哪些情況下該使用 transient

  • 密碼與 token。永遠不要序列化它們!
  • 快取或暫時性資料。例如某個欄位可以「即時」計算得出。
  • 不應或無法序列化的物件。例如資料庫連線、串流、socket 的參照。

transient 欄位的行為特性

當物件被反序列化時,所有標示為 transient 的欄位都會取得預設值。若需要讓它們恢復意義,可以在 readObject 方法中手動填回(重新計算快取、向使用者索取密碼等)。

2. serialVersionUID:類別版本的唯一識別碼

serialVersionUID 是一個 long 型別的特殊靜態欄位,用來定義可序列化類別的「版本」。序列化時會寫出 serialVersionUID 的值;反序列化時,JVM 會將它與目前類別中的值比對。若兩者不相同,會丟出例外,物件也無法還原。

如何宣告 serialVersionUID

非常簡單:

private static final long serialVersionUID = 1L;

通常直接在實作 Serializable 的類別裡宣告:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // ... 其他欄位與方法
}

為什麼需要 serialVersionUID

想像你把某個類別的物件存到檔案,之後又修改了類別結構(新增欄位、重新命名等等)。如果 serialVersionUID 不同,JVM 會認為此類別與舊版本不相容,並不允許你反序列化該物件。這能避免出乎意料的錯誤。

如果不宣告 serialVersionUID 會怎樣?

若沒有明確宣告 serialVersionUID,編譯器會根據類別結構自行產生。但即使是很小的變更(例如新增或刪除欄位)也會改變 serialVersionUID。結果就是:你將無法反序列化由舊版本類別保存的物件。

因此建議一律明確指定 serialVersionUID

示範:serialVersionUID 不相符

1) 先建立類別並序列化物件:

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) 接著修改 serialVersionUID

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 2L; // 從 1L 改成 2L!
    private String username;

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

結果:

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

JVM 清楚地警告:『版本不相容!』

serialVersionUID 該選什麼值?

多半使用簡單的數值(1L2L42L);在大型專案中,IDE 也會產生「很長」的數值。重點是——只有在類別結構發生不相容的變更時,才需要修改它。

3. 實作:transient 欄位與 serialVersionUID 的實際運用

範例:含有 transient 欄位的類別

讓我們修改一個教學應用(例如聯絡人管理器),在使用者類別中新增一個用來保存暫時授權 token 的欄位,而且不該被序列化。

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; // 暫時性的 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 + '\'' +
               '}';
    }
}

現在嘗試序列化並反序列化物件:

import java.io.*;

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

        // 保存物件
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("contact.ser"));
        out.writeObject(c);
        out.close();

        // 還原物件
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("contact.ser"));
        Contact restored = (Contact) in.readObject();
        in.close();

        System.out.println("序列化之前: " + c);
        System.out.println("反序列化之後: " + restored);
    }
}

輸出:

序列化之前: Contact{name='Ivan', phone='+19990001122', sessionToken='token-12345'}
反序列化之後: Contact{name='Ivan', phone='+19990001122', sessionToken='null'}

如你所見,欄位 sessionToken 沒有被還原——它是 transient

範例:關於 serialVersionUID 的小實驗

1) 先以 serialVersionUID = 1L 序列化物件。
2) 然後把 serialVersionUID 改為 2L,再嘗試反序列化同一個檔案。

結果:你會得到 InvalidClassException,就如上所示。

4. 為什麼最好明確指定 serialVersionUID

  • 明確優於隱含。你可以掌控相容性:若類別結構沒有重大變動,就保留原本的 serialVersionUID,物件即可順利反序列化。
  • 自動產生具有風險。任何變更都可能改變計算出的值,進而「破壞」已保存資料的相容性。
  • IDE 很好用。多數 IDE(例如 IntelliJ IDEA)都能自動產生 serialVersionUID

5. 使用 transientserialVersionUID 時的常見錯誤

錯誤 1:忘了把敏感欄位標記為 transient
結果導致密碼或 token 會不小心出現在序列化檔案中。這不只尷尬,更是危險。

錯誤 2:沒有明確宣告 serialVersionUID
類別被修改後,就無法反序列化舊物件:JVM 視其為不相容;但實際上結構可能並未發生關鍵變化。

錯誤 3:不必要地變更 serialVersionUID
如果只是新增 getter 或註解,就不需要改 serialVersionUID——否則舊資料將無法反序列化。

錯誤 4:serialVersionUID 不是 static 或不是 final
此欄位必須宣告為 private static final long serialVersionUID。否則 JVM 不會正確辨識。

錯誤 5:transient 欄位在反序列化後忘了還原。
若其值對物件運作至關重要,請在 readObject 中復原——否則物件可能無法正確運作。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION