Java Basic Serializer

Introduction

序列化其实可以看成是一种机制,按照一定的格式将Java对象的某状态转成介质可接受的形式,以方便存储或传输。

序列化时将Java对象相关的类信息、属性及属性值等等保存起来,反序列化时再根据这些信息构建出Java对象。而过程可能涉及到其他对象的引用,所以这里引用的对象的相关信息也要参与序列化。

将Java对象序列化为二进制文件的Java序列化技术是Java系列技术中一个较为重要的技术点,在大部分情况下,开发人员只需要了解被序列化的类需要实现Serializable接口,使用ObjectInputStream和ObjectOutputStream进行对象的读写。

序列化的作用:

  • 提供一种简单又可扩展的对象保存恢复机制。
  • 对于远程调用,能方便对对象进行编码和解码,就像实现对象直接传输。
  • 可以将对象持久化到介质中,实现对象直接存储。
  • 允许对象自定义外部存储的格式。

Attention

  • 序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量
  • SuperClass不实现Serializable接口,则不会保存SuperClass的状态变量
  • 通过重写writeObject和readObject实现对敏感数据的加密和解密
  • Transient关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后transient变量的值被设为初始值,如int型的是0,对象型的是 null。

SuerperClass Serializer

如果一个子类实现了Serializable接口而父类没有实现该接口,则在序列化子类时,子类的属性状态会被写入而父类的属性状态将不被写入。所以如果想要父类属性状态也一起参与序列化,就要让它也实现 Serializable 接口。

如果父类未实现Serializable接口,则反序列化生成的对象会再次调用父类的构造函数,以此完成对父类的初始化。所以父类属性初始值一般都是类型的默认值。


Externalizable 接口作用

Externalizable接口主要就是提供给用户自己控制序列化内容,虽然transient和ObjectStreamField能定义序列化的字段,但通过Externalizable接口则能更加灵活。

它其实继承了Serializable 接口,提供了writeExternal和readExternal两个方法,也就是在这两个方法内控制序列化和反序列化的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ExternalizableTest implements Externalizable {
public String value = "test";

public ExternalizableTest() {
}

public void writeExternal(ObjectOutput out) throws IOException {
Date d = new Date();
out.writeObject(d);
out.writeObject(value);
}

public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
Date d = (Date) in.readObject();
System.out.println(d);
System.out.println((String) in.readObject());
}
}

serialVersionUID

在序列化操作时,经常会看到实现了Serializable 接口的类会存在一个serialVersionUID属性,并且它是一个固定数值的静态变量。

这个属性有什么作用?其实它主要用于验证版本一致性,每个类都拥有这么一个ID,在序列化的时候会一起被写入流中,那么在反序列化的时候就被拿出来跟当前类的serialVersionUID 值进行比较,两者相同则说明版本一致,可以序列化成功,而如果不同则序列化失败。

Example 1

Case: 两个客户端A和B试图通过网络传递对象数据,A端将对象C序列化为二进制数据再传给B,B反序列化得到C。C对象的全类路径假设为com.abc.model,在A和B端都有这么一个类文件,功能代码完全一致。也都实现了Serializable接口,但是反序列化时总是提示不成功。

关键点:虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,还有一个非常重要的点是两个类的序列化ID是否一致(serialVersionUID)。


序列化存储规则

Example 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.*

class Test : Serializable

fun main(args: Array<String>) {
val out = ObjectOutputStream(FileOutputStream("result.obj"))
val test = Test()
//试图将对象两次写入文件
out.writeObject(test)
out.flush()
System.out.println(File("result.obj").length())
out.writeObject(test)
out.close()
System.out.println(File("result.obj").length())

val oin = ObjectInputStream(FileInputStream("result.obj"))
//从文件依次读出两个文件
val t1 = oin.readObject() as Test
val t2 = oin.readObject() as Test
oin.close()
//判断两个引用是否指向同一个对象
println(t1 === t2)
}

输出结果

1
2
3
42
47
true

误区:两次写入对象,文件大小会变为两倍的大小,反序列化时,由于从文件读取,生成了两个对象,判断相等时应该是输入false才对。

Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的5字节的存储空间就是新增引用和一些控制信息的空间。

反序列化时,恢复引用关系,使得代码中的t1和t2指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。

Example 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*

class Test : Serializable {
var i: Int = 0
}

fun main(args: Array<String>) {
val out = ObjectOutputStream(FileOutputStream("result.obj"))
val test = Test()
test.i = 1
out.writeObject(test)
out.flush()
test.i = 2
out.writeObject(test)
out.close()
val oin = ObjectInputStream(FileInputStream("result.obj"))
val t1 = oin.readObject() as Test
val t2 = oin.readObject() as Test
System.out.println(t1.i)
System.out.println(t2.i)
}

输出

1
2
1
1

误区:写入一次以后修改对象属性值再次保存第二次,然后从result.obj中再依次读出两个对象,输出这两个对象的i属性值,因为保存的状态不一样,因此两个对象的i属性应该不同。

第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。因此在使用一个文件多次writeObject需要特别注意这个问题。


Reference

https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html
https://juejin.im/post/5a7111535188257350518592