在看集合框架源码的过程中,发现很频繁的用到了 transient 关键字。趁此机会,顺便再复习下。
1. transient 的作用
如果我们想把一个对象序列化,在 Java 中,只需要实现 Serializable 接口即可。但是如果不想把对象中的某个属性序列化,就可以使用 transient 修饰该属性。
目前我在实际开发过程中还没遇到过这种情况,网上有博客说当涉及敏感信息(比如密码、银行卡号等)时,可以使用 transient 修饰,但是我一般会再定义一个对象(占用点内存空间),去除其中的敏感信息,然后进行序列化。如果有同学在实际开发中使用过,可以在评论区提出。
序列化一般用在网络 IO(比如 Dubbo 中的 rpc 调用) 和磁盘 IO(写入文件到磁盘)。
2. 简单使用
下面以写入文件到本地磁盘为例,代码如下:
1 | package com.hema.carloan; |
如代码所示,定义一个 User 类,实现 Serializable 接口,下面写个单元测试,代码如下:
1 |
|
上面的代码是将对象序列化后写入磁盘,执行结果为:
写入磁盘的对象:User{name=’zhaoxiaofa’, password=’123456’, mobile=’13812345678’}
下面再写一个读取对象反序列化的单元测试,代码如下:
1 |
|
执行结果为:
读取磁盘的对象:User{name=’zhaoxiaofa’, password=’null’, mobile=’null’}
如我们所想的那样,password 使用 transient 修饰,所以不被序列化。在读出磁盘时它的值为 null 。
疑问: password 没有被序列化可以理解,但是 mobile 没有被 transient 修饰,为什么没有序列化呢?
这个问题可以结合 JVM 内存区域划分来考虑,对象存储在堆内存,而类静态变量在方法区,序列化的是堆内存中的对象。所以,对于类静态变量,不会序列化,与是否被 transient 修饰无关。
友情提示: 很多人会把写入磁盘和读取磁盘的测试放在一个测试方法内,如下所示:
1 |
|
上面的代码执行的结果如下:
原始对象:User{name=’zhaoxiaofa’, password=’123456’, mobile=’13812345678’}
反序列化后的对象:User{name=’zhaoxiaofa’, password=’null’, mobile=’13812345678‘}
一看执行结果,懵逼了。说好的 mobile 不会序列化的呢?其实这个时候的 mobile 读的是内存中的值,这是在一个 JVM 进程中。
为了验证我们的猜想,修改代码如下:
1 |
|
执行结果如下:
原始对象:User{name=’zhaoxiaofa’, password=’123456’, mobile=’13812345678’}
反序列化后的对象:User{name=’zhaoxiaofa’, password=’null’, mobile=’1300000000‘}
打印的 mobile 值是我们修改后的值,而 user.txt 文件中的值肯定不是 1300000000 ,所以是读的内存的值。事实证明,类的静态变量确实不会被序列化。
3. 小结
被 transient 修饰的属性,将不再是对象持久化的一部分,该属性值在序列化后无法获得访问。
一个类的静态变量不管是否被 transient 修饰,均不能被序列化。
4. 集合框架源码中的使用
以 ArrayList 为例,elementData 和 modCount 都被 transient 修饰。
1 | transient Object[] elementData; |
modCount 不序列化可以参考 集合中的modCount ,那为什么 elementData 不序列化呢?如果 elementData 不序列化的话,集合中的数据不就丢了吗?那网络传输或者磁盘存储又有什么用呢?我们来看 ArrayList 的一段源码。
1 | /** |
先看一下注释:以流的方式保存 ArrayList 实例,就是序列化。
这个方法其实就是 ArrayList 手动序列化存储数据的。但是好好地实现 Serializable 接口就能自动序列化,为什么要多次一举自己实现呢?仔细看源码,发现手动存储数据时是从 0 到 size 进行遍历的,size 的值和 elementData 的 length 值是不一样的。举个例子:
1 | List<String> list = new ArrayList<>(); |
如上述代码所示,此时 elementData 的 length 值是容量是 10,而 size 的值为 1(不清楚的同学可以看看 ArrayList 的构造函数和 add 方法)。如果用 transient 修饰 elementData ,那么默认序列化时需要转化为流的大小就比手动序列化大不少。这里也体现了 JDK 源码对性能的追求。
还有一个更重要的点:以 HashMap 为例,计算一个 key 最后放在数组位置的第一步就是使用 Object 类的 hashCode 方法,而这个方法是 native 方法,不同的 JVM 算出来的 hashCode 可能不一样,这样 HashMap 在反序列化时结构就发生了变化。
彩蛋:本文所讲的序列化基于 Serializable,如果是 Externalizable 呢?留到下一篇文章再讲。