Java关键字transient
Java关键字transient
transient是一个Java关键字,它主要用于指示JVM在对象序列化过程中,忽略序列化该变量,即不将该变量写入到序列化流中。通过使用transient,可以避免对临时变量和敏感数据的序列化,并提高序列化性能。
在Java语言中,对象的序列化和反序列化是通过实现Serializable接口和Externalizable接口来完成的。在序列化对象时,JVM会将对象转换为二进制流,并将其写入文件或网络流中。如果对象中某个字段被transient修饰,JVM将忽略该字段的序列化。在反序列化对象时,JVM会将二进制流转换回对象,并自动为Transient字段分配默认值。
transient通常用于指定一些临时变量或敏感数据,不希望在序列化时被记录下来。例如,密码、会话令牌或加密密钥等信息就应该被声明为transient字段。另外,如果某些字段不需要在序列化后传递给其他系统,也可以使用transient来避免序列化。
需要说明的是:
- transient关键字只能修饰变量,不能修饰方法或类。
- 序列化和反序列化是针对对象而言的,对于类变量(静态变量,由static关键字修饰)不能被序列化,所以无需额外添加transient关键字。
- 使用final修饰的变量,不会影响transient关键字的作用效果。
- 如果基于Externalizable 接口实现序列化,因为需要手动指定需要序列化的属性,所以不受transient 关键字的影响。也就是说,基于Externalizable 接口序列化的对象,不受transient 关键字的影响。
- 声明为transient的字段,不会被存储在序列化的二进制流中。因此,序列化还原后变量值将会丢失,并且该字段默认会变成null值 |(JDK原生流工具类)。
- 如果在实现Serializable接口时,有transient关键字实例化的变量,则在反序列化时,该变量将使用默认值初始化,即零值(0、null、false)|(JDK原生流工具类)。
1 |
|
而常见不同的序列化组件,针对transient关键字的处理是不同的。
Externalizable接口
Java中的Externalizable接口是用于自定义对象序列化的方式。通过实现Externalizable接口,可以控制对象的序列化和反序列化过程,以及在序列化和反序列化时写入和读取哪些字段。实现Externalizable接口需要实现两个方法:writeExternal( ) 和 readExternal( )。其中,writeExternal( )方法用于将对象的字段写入输出流,而readExternal( )方法用于从输入流中读取字段并设置到对象中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class User implements Externalizable {
private String name;
private transient Integer age;
/**
* 自定义序列化
*
* @param objectOutput
* @throws IOException
*/
public void writeExternal(ObjectOutput objectOutput) throws IOException {
objectOutput.writeUTF(name);
objectOutput.writeInt(age);
}
/**
* 自定义反序列化
*
* @param objectInput
* @throws IOException
* @throws ClassNotFoundException
*/
public void readExternal(ObjectInput objectInput) throws IOException, ClassNotFoundException {
name = objectInput.readUTF();
age = objectInput.readInt();
}
}序列化ID
在实现
Serializable
接口的类中,不需要显式地设置序列化ID。Java会自动生成一个序列化ID,如果没有显式指定的话。但是,建议在类中显式地声明一个serialVersionUID
,这样可以确保在类结构发生变化时,仍然能够向后兼容。Serializable自动生成序列化ID
Java在序列化一个类时,如果没有显式地声明
serialVersionUID
,它会根据类的结构自动生成一个序列化ID。这个自动生成的序列化ID是基于类的结构计算得出的,因此它是与类的内容相关的。如果类的结构发生变化,比如添加或删除字段、修改字段的类型或者顺序等,那么自动生成的序列化ID也会发生变化。具体而言,Java使用一个称为“Object Stream Protocol Version”(对象流协议版本)的规范,通过对类的结构进行哈希计算,生成一个64位的long型数字作为序列化ID。这个哈希计算的过程是基于类的以下信息:
- 类的名称(包括包名)。
- 类的修饰符(public、private等)。
- 类的接口。
- 类的字段(包括字段的名称、类型、修饰符等)。
- 类的方法(包括方法的名称、参数列表、返回类型、修饰符等)。
通过对上述信息进行哈希计算,生成的数字就是自动生成的序列化ID。由于计算基于类的结构,因此如果类的结构发生变化,这个自动生成的序列化ID也会随之变化。
需要注意的是,这个自动生成的序列化ID并不是唯一标识一个类的最佳方式,因为它对于不同的JVM实现和编译器可能会有差异。因此,在实际应用中,建议显式声明
serialVersionUID
,以确保更好的序列化兼容性。手动声明serialVersionUID
可以使开发者在类结构发生变化时有更多的控制权,确保向后兼容。当类的结构发生变化时,如果原先序列化的类和反序列化时的类结构不一致,即序列化时的版本与反序列化时的版本不匹配,Java会抛出
InvalidClassException
异常。这是因为Java在反序列化时会检查类的序列化ID是否匹配,如果不匹配就会认为类的版本不一致,从而抛出异常。因此,即使是使用Java自动生成的序列化ID,也需要在类的结构发生变化时进行兼容性的处理,以确保向后兼容。建议在类中显式地声明一个
serialVersionUID
,这样可以确保在类结构发生变化时,仍然能够向后兼容。Externalizable不需要序列化ID
而在实现
Externalizable
接口的类中,则不需要设置serialVersionUID
,因为Externalizable
接口不会自动生成序列化ID。相反,它要求程序员手动实现writeExternal
和readExternal
方法来控制序列化和反序列化的过程。
Fastjson
序列化
1 | TransientDemo transientDemo = new TransientDemo(); |
序列化结果:
1 | {"name":"Maple"} |
反序列化:
1 | String jsonStr = "{\"name\":\"Maple\",\"password\":\"106\"}"; |
反序列化结果:
1 | aTransient = TransientDemo(name=Maple, password=106) |
fastjson和JDK原生的流处理工具类序列化对比,transient关键字标记的字段都不会序列化到结果中。但反序列化时,fastjson三方组件会恢复transient关键字标记的字段。
Jackson
序列化:
1 | TransientDemo transientDemo = new TransientDemo(); |
序列化结果:
1 | {"name":"Maple","password":"nba106118"} |
反序列化:
1 | String jsonStr = "{\"name\":\"Maple\",\"password\":\"106\"}"; |
反序列化结果:
1 | demo1 = TransientDemo(name=Maple, password=106) |
jackson和JDK原生的流处理工具类序列化对比,transient关键字完全没有效果,序列化和反序列化都会对transient修饰的字段进行处理。
Gson
序列化:
1 | TransientDemo transientDemo = new TransientDemo(); |
序列化结果:
1 | {"name":"Maple"} |
反序列化:
1 | String jsonStr = "{\"name\":\"Maple\",\"password\":\"106\"}"; |
反序列化结果:
1 | demo = TransientDemo(name=Maple, password=null) |
gson和JDK原生的流处理工具类序列化对比,transient关键字处理效果和JDK原生流工具一样。序列化时忽略transient修饰的属性,反序列化时将transient修饰的属性值置为属性类型的默认初始值。
从上述Demo看来,常用的序列化组件fastjson、jackson、gson,只有gson对于transient关键字属性的序列化和反序列化和JDK原生序列化工具一致。
参考文档:
Serializable 接口和 Externalizable 接口的区别