前言
最近在看 HashMap
源代码的时候,发现链表 table
数组采用了transient
关键字,笔者当时感觉对 transient
关键字即陌生但又有似曾相识,所以花了一些时间简要的总结了下使用transient
关键字的一些基本常识,希望对你们也有些帮助,让我们一起进步,一起牛逼吧。
说起 transient
关键字,不得不提对象的 序列化
的,因为我们常常需要在网络上以对象(数据)的二进制方式传输数据,这里涉及到发送方序列化对象,接收方反序列化对象的过程。
那什么是序列化/反序列化?
“
Java
中对象的序列化指的是将对象转换成以字节序列的形式来表示,这些字节序列包含了对象的数据和信息,一个序列化后的对象可以被写到数据库或文件中,也可用于网络传输。一般地,当我们使用缓存cache
(内存空间不够有可能会本地存储到硬盘)或远程调用rpc
(网络传输)的时候,经常需要让实体类实现Serializable
接口,目的就是为了让其可序列化。当然,序列化后的最终目的是为了反序列化,恢复成原先的Java对象实例。所以序列化后的字节序列都是可以恢复成Java对象的,这个过程就是反序列化。
在对象的序列化/反序列化过程中,我们经常有这种需求,就是非必要字段不必进行序列化。
例如有一个对象有三个字段 field1
、field2
、field3
,发送方不想让字段 field3
被序列化,因为这里面可能涉及到一些敏感信息不想被接收方知道,那有没有办法解决这个问题呢?
其实聪明的 Java
作者早就为我们量身定做了 transient
关键字!
简单来说,被 transient
关键字修饰过的成员属性不能被序列化,transient
关键字只能修饰变量,而不能修饰方法和类。
transient
关键字修饰的。transient
关键字修饰过的属性不能被序列化,也就是说被 transient
修饰过的属性,在对对象序列化后,是无法访问到该属性的。transient
修饰过,不能被序列化。首先,我们看个例子,有个产品对象 Product
,包括价格
、数量
、总价
三个字段,那么总价
可以通过 价格
乘以 数量
推导出来。
我们以查询某个产品 API
接口为例,通过产品 ID
,查询返回一个产品对象。
public class Product {
private int amounts;
private int price;
private int sum;
}
通过 Gson
序列化后把 json
数据返回给前端,这时的 sum
字段是没有经过 transient
修饰过的,所以能够正常序列化。
{"amounts":3,"price":2,"sum":6}
假设我们的产品对象 Product
的 sum
属性加上 transient
关键字修饰:
public class 产品对象 Product 的 sum {
private int amounts;
private int price;
private transient int sum;
}
然后我们试着初始化 Product
,并用 Gson
的 toJson()
方法序列化输出 json
格式的结果。
public static void main(String[] args) {
Product p = new Product();
p.setAmounts(3);
p.setPrice(2);
p.setSum(p.getAmounts() * p.getPrice());
String json = new Gson().toJson(p);
System.out.println(json);
}
这时控制台是没有打印出 sum
字段的。
{"amounts":3,"price":2}
我们看到,sum
属性被 transient
修饰后,是不会被 Gson
序列化输出的,这里就引出了使用 transient
关键字一个很重要的概念:对象属性推导。
“如果一个对象的属性值可以通过其他属性或者方法推理出来的,那么该属性就没必要被序列化了。
借此我们以 Gson
来分析被 transient
修饰过的属性不能被序列化过程。
首先,调用 Gson
的 toJson()
方法,传入 Product
对象。
new Gson().toJson(product)
根据传入的产品对象,获取 Product
对象的 class
类型:typeOfSrc
,最后 找到对应的对象解析适配器工厂。
toJson(Object src, Type typeOfSrc, JsonWriter writer)
TypeAdapter<?> adapter = getAdapter(TypeToken.get(typeOfSrc));
for (TypeAdapterFactory factory : factories) {
// 得到ReflectiveTypeAdapterFactory
TypeAdapter<T> candidate = factory.create(this, type);
}
通过适配器 ReflectiveTypeAdapterFactory
工厂的 create()
方法,我们找到 getBoundFields
方法。
new Adapter<T>(constructor, getBoundFields(gson, type, raw));
for (Field field : fields) {
boolean serialize = excludeField(field, true);
boolean deserialize = excludeField(field, false);
...
}
这个方法做了两件事情:
transient
关键字修饰过的属性通过 excludeField()
方法,剔除被 transient
修饰过的属性,其规则是通过位运算 "&"
判断 modifiers
属性与对象属性的 field.getModifiers()
的值是否一致,来证明该属性是否被 transient
修饰过,如果是为真,表示剔除该属性,不进行序列化。
public boolean excludeField(Field field, boolean serialize) {
// 通过 if 判断 modifiers 属性
// private int modifiers = Modifier.TRANSIENT | Modifier.STATIC;
if ((modifiers & field.getModifiers()) != 0) {
return true;
}
}
另外根据 modifiers
属性定义 Modifier.TRANSIENT | Modifier.STATIC
两种类型,一种是 tranient
,另一种是 static
静态类型。
Modifier.STATIC:静态类型
由约定三、我们知道,静态变量不会被序列化。
代码 debug
到此,我们已经知道 Gson
是如何证明对象是否存在被 transient
修饰过属性以及如何过滤掉的完整过程。
想要解决这个问题,首先还要再重提一下对象的序列化方式,Java
序列化提供两种方式。
一种是实现 Serializable
接口,另一种是实现 Exteranlizable
接口。
实现 Exteranlizable
接口需要重写 writeExternal
和 readExternal
方法,它的效率比 Serializable
高一些,并且可以决定哪些属性需要序列化(即使是 transient
修饰的),但是对大量对象,或者重复对象,则效率低。
从上面的这两种序列化方式,我想你已经看到了,使用 Exteranlizable
接口实现序列化时,我们自己指定那些属性是需要序列化的,即使是 transient
修饰的。下面就验证一下
首先我们定义 User1
类:这个类是被 Externalizable
接口修饰的
然后我们就可以测试了
上面,代码分了两个方法,一个是序列化,一个是反序列化。里面的代码和一开始给出的差不多,只不过,User1
里面少了 age
这个属性。
然后看一下结果:
结果基本上验证了我们的猜想,也就是说,实现了 Externalizable
接口,哪一个属性被序列化是我们手动去指定的,即使是 transient
关键字修饰也不起作用。
Gson
方式来验证 tranient
关键字不能序列化的使用场景。Externalizable
接口,如果手动去指定属性序列化的,即使是 transient
关键字修饰也不起作用。java
的 io
包下的 ObjectInputStream
和 ObjectOutputStream
两个对象输入输出流也可以验证,这里就不再做赘述,感兴趣的朋友可以在网上找找例子。