Fork me on GitHub

transient关键字

在看集合框架源码的过程中,发现很频繁的用到了 transient 关键字。趁此机会,顺便再复习下。

1. transient 的作用

如果我们想把一个对象序列化,在 Java 中,只需要实现 Serializable 接口即可。但是如果不想把对象中的某个属性序列化,就可以使用 transient 修饰该属性。

目前我在实际开发过程中还没遇到过这种情况,网上有博客说当涉及敏感信息(比如密码、银行卡号等)时,可以使用 transient 修饰,但是我一般会再定义一个对象(占用点内存空间),去除其中的敏感信息,然后进行序列化。如果有同学在实际开发中使用过,可以在评论区提出。

序列化一般用在网络 IO(比如 Dubbo 中的 rpc 调用) 和磁盘 IO(写入文件到磁盘)。

2. 简单使用

下面以写入文件到本地磁盘为例,代码如下:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.hema.carloan;

import java.io.Serializable;

/**
* @desc:
* @Author: zhaoxiaofa
* @Date: 2019-09-19 21:51
*/
public class User implements Serializable {

private static final long serialVersionUID = -3642853987965677236L;

private String name;

private static String mobile;

private transient String password;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getMobile() {
return mobile;
}

public void setMobile(String mobile) {
this.mobile = mobile;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", password='" + password + '\'' +
", mobile='" + mobile + '\'' +
'}';
}
}

如代码所示,定义一个 User 类,实现 Serializable 接口,下面写个单元测试,代码如下:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void userWriteTest() throws Exception {
User user = new User();
user.setName("zhaoxiaofa");
user.setMobile("13812345678");
user.setPassword("123456");
System.out.println("写入磁盘的对象:" + user.toString());
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("/user.txt"));
o.writeObject(user);
o.close();
}

上面的代码是将对象序列化后写入磁盘,执行结果为:

写入磁盘的对象:User{name=’zhaoxiaofa’, password=’123456’, mobile=’13812345678’}

下面再写一个读取对象反序列化的单元测试,代码如下:

1
2
3
4
5
6
7
@Test
public void userReadTest() throws Exception{
ObjectInputStream in = new ObjectInputStream(new FileInputStream("/user.txt"));
User object = (User) in.readObject();
System.out.println("读取磁盘的对象:" + object.toString());
in.close();
}

执行结果为:

读取磁盘的对象:User{name=’zhaoxiaofa’, password=’null’, mobile=’null’}

如我们所想的那样,password 使用 transient 修饰,所以不被序列化。在读出磁盘时它的值为 null 。

疑问: password 没有被序列化可以理解,但是 mobile 没有被 transient 修饰,为什么没有序列化呢?

这个问题可以结合 JVM 内存区域划分来考虑,对象存储在堆内存,而类静态变量在方法区,序列化的是堆内存中的对象。所以,对于类静态变量,不会序列化,与是否被 transient 修饰无关。

友情提示: 很多人会把写入磁盘和读取磁盘的测试放在一个测试方法内,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void userTest() throws Exception {
User user = new User();
user.setName("zhaoxiaofa");
user.setMobile("13812345678");
user.setPassword("123456");
System.out.println("原始对象:" + user.toString());
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("/user.txt"));
o.writeObject(user);
o.close();

ObjectInputStream in = new ObjectInputStream(new FileInputStream("/user.txt"));
User object = (User) in.readObject();
System.out.println("反序列化后的对象:" + object.toString());
in.close();
}

上面的代码执行的结果如下:

原始对象:User{name=’zhaoxiaofa’, password=’123456’, mobile=’13812345678’}
反序列化后的对象:User{name=’zhaoxiaofa’, password=’null’, mobile=’13812345678‘}

一看执行结果,懵逼了。说好的 mobile 不会序列化的呢?其实这个时候的 mobile 读的是内存中的值,这是在一个 JVM 进程中。

为了验证我们的猜想,修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void userTest() throws Exception {
User user = new User();
user.setName("zhaoxiaofa");
user.setMobile("13812345678");
user.setPassword("123456");
System.out.println("原始对象:" + user.toString());
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("/user.txt"));
o.writeObject(user);
o.close();

// 在写入磁盘之后修改 mobile 的值
user.setMobile("1300000000");

ObjectInputStream in = new ObjectInputStream(new FileInputStream("/user.txt"));
User object = (User) in.readObject();
System.out.println("反序列化后的对象:" + object.toString());
in.close();
}

执行结果如下:

原始对象: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
2
transient Object[] elementData;
protected transient int modCount = 0;

modCount 不序列化可以参考 集合中的modCount ,那为什么 elementData 不序列化呢?如果 elementData 不序列化的话,集合中的数据不就丢了吗?那网络传输或者磁盘存储又有什么用呢?我们来看 ArrayList 的一段源码。

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
/**
* Save the state of the <tt>ArrayList</tt> instance to a stream (that
* is, serialize it).
*
* @serialData The length of the array backing the <tt>ArrayList</tt>
* instance is emitted (int), followed by all of its elements
* (each an <tt>Object</tt>) in the proper order.
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();

// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);

// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}

if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

先看一下注释:以流的方式保存 ArrayList 实例,就是序列化。

这个方法其实就是 ArrayList 手动序列化存储数据的。但是好好地实现 Serializable 接口就能自动序列化,为什么要多次一举自己实现呢?仔细看源码,发现手动存储数据时是从 0 到 size 进行遍历的,size 的值和 elementData 的 length 值是不一样的。举个例子:

1
2
List<String> list = new ArrayList<>();
list.add("test");

如上述代码所示,此时 elementData 的 length 值是容量是 10,而 size 的值为 1(不清楚的同学可以看看 ArrayList 的构造函数和 add 方法)。如果用 transient 修饰 elementData ,那么默认序列化时需要转化为流的大小就比手动序列化大不少。这里也体现了 JDK 源码对性能的追求。

还有一个更重要的点:以 HashMap 为例,计算一个 key 最后放在数组位置的第一步就是使用 Object 类的 hashCode 方法,而这个方法是 native 方法,不同的 JVM 算出来的 hashCode 可能不一样,这样 HashMap 在反序列化时结构就发生了变化。

彩蛋:本文所讲的序列化基于 Serializable,如果是 Externalizable 呢?留到下一篇文章再讲。

本文标题:transient关键字

原始链接:https://zhaoxiaofa.com/2019/01/03/transient关键字/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。