0x00 序列化与反序列化

其实这应该是一对很简单的概念,但是有一些资料用了一些很高大上的语句和词汇,整得初学者往往很头大。简单来说,序列化的主要用途主要就是为了传递或保存整个对象。对象存储在内存中的,在传递过程中需要将其以某种形式表现出来,先举个简单的例子,比如将一个对象序列化为一个JSON字符串的形式。

1
2
3
4
5
public class Person {
private String name; // ="Qun"
private int age; // =22
// 省略getter, setter
}

序列成JSON字符串就成为了:

1
2
3
4
{
"name":"Qun",
"age":22
}

这个JSON字符串的形式在网络上传递就很方便了,也可以将其保存成一个JSON文件做持久化存储,这其实就是序列化的意义。同样,我也可以通过这个JSON字符串来还原回一个Person对象,这也就是反序列化的意义。

注意:如果一个类A为可序列化的,那他的子类也是可序列化的

0x01 Java的序列化算法过程

将一个标记为可序列化的对象序列化的过程如下:

  • 写出这个对象所属类的metadata
  • 从当前对象所属类开始,依次向其基类递归写出类中成员的描述信息,包括成员名称、类型等,直到抵达java.lang.Object
  • 从最基类开始,依次向下写出各成员变量的值

此处参考:

The Java serialization algorithm revealed

上述内容摘自其Java’s serialization algorithm小节

0x02 Serializable接口

在实际中,序列化一个对象是将其保存成了一个二进制的字节数组,为此实体类必需实现java.io.Serializable接口,以告诉Java虚拟机这是一个可序列化的类。同时在Java的序列化机制中还要检查版本的一致性,所以在序列化后的结果中,我们往往还会加入一个serialVersionUID参数,这是一个long类型的参数。在进行反序列化的过程中,首先比较的就是这个参数的值是否与本地反序列化实体类的这个参数的值是否相等,如果相等则可以进行反序列化,否则抛出InvalidClassException异常,不能进行反序列化。

serialVersionUID的生成有2种方式:

  • 默认值为1L
  • 根据类名、接口名、成员方法以及属性等来生成一个64位的Hash字段

同时如果实现java.io.Serializable接口的实体类没有显式定义一个名为serialVersionUID、类型为long的变量时,Java序列化机制会根据编译的.class文件自动生成一个serialVersionUID

注意:

如果我们打开java.io.Serializable这个接口的代码,会发现这个接口是空的,因为它仅起到一个标识的作用。

0x03 序列化与反序列化的实现

要让我们自己去实现序列化与反序列化的过程,即实现如下2个接口:

1
2
3
4
5
6
// 重写序列化过程
private void writeObject(java.io.ObjectOutputStream out)
throws IOException
// 重写反序列化过程
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException

参见我的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Person implements Serializable {
private String name;
private int age;

private void writeObject(java.io.ObjectOutputStream out)
throws IOException {
String serialized = name + "," + age;
out.writeObject(serialized);
}

private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
String serialized = (String) in.readObject();
name = serialized.substring(0, serialized.indexOf(','));
age = Integer.parseInt(serialized.substring(serialized.indexOf(',') + 1));
}
// 省略getter, setter
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
Person a = new Person();
a.setName("Qun");
a.setAge(19);
try {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("F:/result.obj"));
out.writeObject(a);
} catch (IOException e) {
e.printStackTrace();
}
}
}

我们在此处将序列化后的结果存放到一个文件中,我们可以使用16进制编辑器(比如UltraEdit)或者记事本打开这个文件,我们可以看到,前面是Java为我们写入的一些必要的信息,包括包名类名以及相关的字段及其类型,同时在最后我们可以看到,我们重写后的序列化的结果Qun,19

同样,我们再创建一个程序,使之读取出序列化的结果来:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
try {
ObjectInputStream in = new ObjectInputStream(new FileInputStream("F:/result.obj"));
Person a = (Person) in.readObject();
System.out.println(a.getName() + " " + a.getAge());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

0x04 transient关键字

说实话这个关键字我接触地比较晚,因为在我的领域内不怎么常用,我第一次接触的时候是在HashMap的源代码里:

1
2
3
4
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;

transient关键字用于修饰成员变量,被修饰的成员不被序列化或反序列化,这主要适用于一些此值可以被其他值推导出来的情况,比如一个记录长方形的类,里面有长度、宽度、面积三个成员变量,面积可以通过长度乘以宽度推导出来,这样我们在序列化的时候就没必要把面积也序列化出来,只需要序列化长度和宽度即可。同理在上述HashMap的源代码里,size表示存储的键值对的个数,modCount表示这个HashMap对象被修改的次数,同样没必要序列化出来。一是为了节省空间,二是因为某些字段的值在反序列化时可能需要重新计算

同时需要注意的是静态变量也是不会被序列化的