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,对 boolean 是 false。
实践上如何工作?
让我们做个小实验。先序列化一个用户:
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 值?
多数情况下使用简单的值(1L、2L、42L),而在大型项目中 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. 使用 transient 和 serialVersionUID 时的常见错误
错误 1:忘了把敏感字段标记为 transient。
结果,密码或令牌会意外出现在序列化文件中。这不仅尴尬,而且危险。
错误 2:没有显式声明 serialVersionUID。
类被修改后,无法反序列化旧对象:JVM 判定它们不兼容,尽管本质上结构可能并未发生破坏性变化。
错误 3:不必要地更改 serialVersionUID。
如果你只是新增了 getter 或注释,就不需要修改 serialVersionUID——否则旧数据将无法反序列化。
错误 4:serialVersionUID 不是 static 或不是 final。
该字段必须声明为 private static final long serialVersionUID。否则 JVM 不会正确识别。
错误 5:transient 字段在反序列化后忘了恢复。
如果该值对对象的工作至关重要,请在 readObject 中恢复它——否则对象可能工作不正常。
GO TO FULL VERSION