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("john", "qwerty123");

        // Save the object to a file
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.ser"));
        out.writeObject(user);
        out.close();

        // Now read the object back
        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: john
Password: null

如你所见,password 字段没有被恢复——它是 transient,因此被序列化器忽略。

在哪里以及为何使用 transient

  • 密码和令牌。 切勿序列化它们!
  • 缓存或临时数据。 例如,可以“即时”计算出来的字段。
  • 不应或无法被序列化的对象。 例如指向数据库连接、流、套接字的引用。

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 字段的类

我们来改造一个教学应用(例如联系人管理器),在用户类中添加一个用于存储临时授权令牌的字段,该字段不应进入序列化。

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; // 临时令牌

    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
结果,密码或令牌会意外出现在序列化文件中。这不仅尴尬,而且危险。

错误 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