在Java编程中为了进行网络传输或方便磁盘存储,我们经常需要对Java对象进行序列化,Java的对象序列化就是将那些实现了Serializable接口的对象转换成字符序列,并能够在需要时反序列化为原来的Java对象。因此,只要对象实现了Serializable接口(该接口不包含任何方法),即可对其进行序列化。
Java内置序列化与反序列化
1 | import java.io.Serializable; |
TestObject.class是我们定义的测试类TestObject,该类实现了Serializable接口,类中包含了字段a和b。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import java.io.IOException;
import java.io.ObjectOutputStream;
public class TestWrite {
public static void main(String[] args) {
TestObject serialize = new TestObject(7, 18);
try {
FileOutputStream fileOutputStream = new FileOutputStream("serialize.out");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(serialize);
} catch (IOException e) {
e.printStackTrace();
}
}
}
接下来,我们编写序列化测试代码TestWrite.class,将新建一个TestObject对象,并利用Java内置序列化,将测试对象序列化后存入serialize.out文件中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class TestRead {
public static void main(String[] args) {
try {
FileInputStream fileInputStream = new FileInputStream("serialize.out");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
TestObject serialize = (TestObject) objectInputStream.readObject();
System.out.println(serialize);
} catch (Exception e) {
e.printStackTrace();
}
}
}
最后,我们编写反序列化测试代码TestRead.class,读取serialize.out文件中的数据,并对其反序列化为Java对象,类型转换为TestObject类,并打印出该对象,输出结果为:a: 7 b: 18
。由此我们完成了序列化和反序列的过程。
serialVersionUID作用和用法
现在出现另一个问题,如果在我们读取serialize.out文件,进行反序列化时,我们修改了TestObject.class类,在类中添加新的字段int c
,这时候我们进行反序列化,程序会报异常java.io.InvalidClassException: TestObject;
之所以会报异常,是因为Java在序列化和反序列化的过程中,会涉及到很多检查,其中就包括对serialVersionUID的检查。serialVersionUID可以由类指定,也可以不指定,如果不指定(比如我们上面例子中),Java会根据class计算serialVersionUID,只要类变化,计算出来的serialVersionUID也会发生变化。在反序列化时,如果发现类中serialVersionUID与之前序列化的serialVersionUID对不上号,就会抛出java.io.InvalidClassException
异常。所以我们上面例子中,在添加了新的字段,改变了TestObject.class类,该类的serialVersionUID与当初序列化时的发生了改变,所以报出异常。
但是在实际应用中,我们经常会碰到这种需求,比如:在大数据处理中,Flume日志收集组件将结构化日志对应的类对象进行序列化,通过网络传输写入RocketMQ队列,实时数据分析集群JStorm中存在多个Topology作业,都从RocketMQ队列中读取序列化的数据,并反序列化后得到相应的结构化日志对象。这是典型的大数据实时处理架构,在实际应用中,如果我们修改了Flume端结构日志对应的类结构,如添加或删减了某些字段,这时候,我们不得不清空RocketMQ队列中修改前的数据,并对JStorm中使用到该类的多个Topology作业做出调整,这是一件相当繁琐的事情。显示这不符合程序开发的规则,代码不能实现“向下兼容”。
我们真正需要的是,将M类对象序列化之后,需要版本升级,修改M类,希望反序列化的时候还能识别之前版本的序列化对象。要实现这种“向下兼容”,相当简单,只需要我们显示的指定序列化对象的serialVersionUID即可。
serialVersionUID有两种显示的生成方式,一种是默认的1L,一种是根据类及其成员等属性生成的一个64位哈希码。我们使用的集成开发环境Eclipse和Intellij IDEA可以帮我们自动生成serialVersionUID。以IDEA为例,IDEA默认并没有开启自动生成serialVersionUID的功能,需要我们手动开启该功能,在File->Setting->Inspections->Serializable class without “serialVersionUID”选项上打上对勾,即可开启该功能。
开启该功能后,我们在实现了Serializable接口的类名上按下Alt+Enter,并选择添加serialVersionUID即可。
加入serialVersionUID后的TestObject.clas如下所示,然后我们重新运行TestWrite,对其该类对象进行序列化并写入serialize.out文件,然后我们TestObject.class类中进行添加int c
字段或删除long b
字段等操作,发现TestRead可以正常反序列化得到TestObject对象,完美实现了“向下兼容”。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import java.io.Serializable;
public class TestObject implements Serializable {
private static final long serialVersionUID = -4761019091046434326L;
private int a;
private long b;
public TestObject(int a, long b){
this.a = a;
this.b = b;
}
public String toString() {
return "a: " + a + " b: "+ b;
}
}
当序列化时类不变,反序列化时,在TestObject类中添加字段,则新增字段在反序列化时被赋予默认值;若删除了字段,则在反序列化时会忽略被删减的字段。反过来,序列化时类增加或删减字段,反序列化时类不变的情况,与以上情况相同。