对象序列化
Java对象序列化(Serialize)是指将Java对象写入IO流,反序列化(Deserilize)则是从IO流中恢复该Java对象。
对象序列化将程序运行时内存中的对象以字节码的方式保存在磁盘中,或直接通过网络进行传输(例如web中的HttpSession,或者J2EE中的RMI参数及返回值),以便通过反序列化的方式将字节码恢复成对象来使用。
所有可能在网络上传输对象的类都应该可序列化,通常分布式应用需要跨平台,跨网络,所以要求所有传递的参数及返回值都可序列化。
通常让需要被序列化和的类实现Serializable接口,调用序列化流对象(ObjectOutputStream/ObjectInputStream)的writeObject和readObject就可以实现序列化,
另外,通过实现Externlizable接口也可以实现序列化,但是必须要重写writeObject和readObject这两个方法才行。
使用Serializable序列化
只需要目标类实现了Serializable接口即可,不需要重写任何方法,直接调用序列化流对象(ObjectOutputStream/ObjectInputStream)的writeObject和readObject。
假如现在有一个Person类需要序列化,
1 class Person implementsjava.io.Serializable{2 publicString getName() {3 returnname;4 }5 public voidsetName(String name) {6 this.name =name;7 }8 public intgetAge() {9 returnage;10 }11 public void setAge(intage) {12 this.age =age;13 }14 privateString name;15 private intage;16
17 public Person(String name, intage) {18 System.out.println("有参数构造器");19 this.name =name;20 this.age =age;21 }22 }23
在测试类中,使用流对象(ObjectOutputStream)的writeObject就可以将对象序列化到具体文件,
使用流对象(ObjectInputStream)的readObject就可以从指定文件反序列化对象
1 public classObjectIO {2 public static voidwriteObject() {3 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.txt"))) {4 Person per = new Person ("孙悟空",500);5 oos.writeObject(per);6 } catch(IOException e) {7 e.printStackTrace();8 }9 }10
11 public static void readObject() throwsFileNotFoundException, IOException, ClassNotFoundException {12 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.txt"))) {13 Person p =(Person)ois.readObject();14 System.out.println("name: "+p.getName()+", age: "+p.getAge());15 }16 }17
18 public static void main(String[] args) throwsFileNotFoundException, ClassNotFoundException, IOException {19 writeObject();20 //不会调用构造器
21 readObject();22 }23 }24
执行结果,
1 有参数构造器2 name: 孙悟空, age: 500
对象引用的序列化
如果某个类的成员变量不是基本类型,而是另一个类的引用类型(例如 Teacher 类中有个成员变量 private Person student),那么这个引用类(Person)必须是可序列化的,当前类(Teacher)才可以序列化,否则会抛出java.io.NotSerializableException异常。
对象不会被重复序列化
序列化对象将会产生一个序列号,每次序列化对象时会先检查是否已经序列化过该对象,只有未被序列化的对象才序列化,已被序列化的对象则直接输出序列号,而不会重新序列化该对象。
即:对象只有第一次序列化才生效。
例如下面的例子,
1 packageio;2
3 importjava.io.FileInputStream;4 importjava.io.FileNotFoundException;5 importjava.io.FileOutputStream;6 importjava.io.IOException;7 importjava.io.ObjectInputStream;8 importjava.io.ObjectOutputStream;9
10
11 classPerson {12 publicString getName() {13 returnname;14 }15 public voidsetName(String name) {16 this.name =name;17 }18 public intgetAge() {19 returnage;20 }21 public void setAge(intage) {22 this.age =age;23 }24 privateString name;25 private intage;26
27 public Person(String name, intage) {28 System.out.println("有参数构造器");29 this.name =name;30 this.age =age;31 }32 }33
34 class Teacher implementsjava.io.Serializable {35 publicString getName() {36 returnname;37 }38 public voidsetName(String name) {39 this.name =name;40 }41 publicPerson getStudent() {42 returnstudent;43 }44
45
46 public classObjectIO {47
48 public static void writeTeacher() throwsFileNotFoundException, IOException, ClassNotFoundException {49 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {50 Person per = new Person("孙悟空" , 500);51 Teacher t1 = new Teacher("唐僧", per);52 Teacher t2 = new Teacher("菩提祖师", per);53
54 //下面四行只会序列化三个对象,其中t1, t2 将引用同一个对象
55 oos.writeObject(t1);56 oos.writeObject(t2);57 oos.writeObject(per);58 oos.writeObject(t2);59 } catch(IOException e) {60 e.printStackTrace();61 }62
63 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"))) {64 //反序列化需要按照序列化的顺序取对象
65 Teacher t1 =(Teacher)ois.readObject();66 Teacher t2 =(Teacher)ois.readObject();67 Person p =(Person)ois.readObject();68 Teacher t3 =(Teacher)ois.readObject();69 System.out.println("t1的student引用和p是否相同: "+ (t1.getStudent() ==p));70 System.out.println("t2的student引用和p是否相同: "+ (t2.getStudent() ==p));71 System.out.println("t2和t3是否是同一个对象: "+ (t2 ==t3));72 } catch(IOException e) {73 e.printStackTrace();74 }75 }76
77
78 public static void main(String[] args) throwsFileNotFoundException, ClassNotFoundException, IOException {79 writeTeacher();80 }81 }
View Code
上面的例子中,Teacher 类中有个成员变量 private Person student, 虽然实例化了两个Teacher对象t1 t2, 连同Person类一起都进行了序列化,但是从反序列化结果中看到这三个Person对象完全相同。
执行结果,
1 有参数构造器2 t1的student引用和p是否相同: true
3 t2的student引用和p是否相同: true
4 t2和t3是否是同一个对象: true
可变对象序列化
由于Java对象只会序列化一次,当可变对象在序列化之后发生了变化,即使再进行一次序列化,也不能改变序列化对象的值,反序列化之后依然是第一次序列化的值,例如下面的例子,
1 public static void mutable() throwsFileNotFoundException, IOException, ClassNotFoundException {2 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("mutable.txt"));3 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("mutable.txt"))) {4 Person per = new Person("孙悟空" , 500);5 oos.writeObject(per);6 per.setName("猪八戒");7 //per在前面已经序列化过,这里再序列化不会生效
8 oos.writeObject(per);9 Person p1 =(Person)ois.readObject();10 Person p2 =(Person)ois.readObject();11 System.out.println(p1==p2);12 System.out.println(p2.getName());13 } catch(IOException e) {14 e.printStackTrace();15 }16 }17
18 public static void main(String[] args) throwsFileNotFoundException, ClassNotFoundException, IOException {19 mutable();20 }
上面程序在第5行序列化之后,在第6行修改了对象的值,然后又在第8行序列化一次。但是通过反序列化之后发现,name的值依然是第一次序列化的值,执行结果,
1 有参数构造器2 true
3 孙悟空
以上完整代码,
1 packageio;2
3 importjava.io.FileInputStream;4 importjava.io.FileNotFoundException;5 importjava.io.FileOutputStream;6 importjava.io.IOException;7 importjava.io.ObjectInputStream;8 importjava.io.ObjectOutputStream;9
10
11 class Person implementsjava.io.Serializable {12 publicString getName() {13 returnname;14 }15 public voidsetName(String name) {16 this.name =name;17 }18 public intgetAge() {19 returnage;20 }21 public void setAge(intage) {22 this.age =age;23 }24 privateString name;25 private intage;26
27 public Person(String name, intage) {28 System.out.println("有参数构造器");29 this.name =name;30 this.age =age;31 }32 }33
34 class Teacher implementsjava.io.Serializable {35 publicString getName() {36 returnname;37 }38 public voidsetName(String name) {39 this.name =name;40 }41 publicPerson getStudent() {42 returnstudent;43 }44 public voidsetStudent(Person student) {45 this.student =student;46 }47 privateString name;48 privatePerson student;49 publicTeacher(String name, Person student) {50 this.name =name;51 this.student =student;52 }53 }54
55 public classObjectIO {56 public static voidwriteObject() {57 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.txt"))) {58 Person per = new Person ("孙悟空",500);59 oos.writeObject(per);60 } catch(IOException e) {61 e.printStackTrace();62 }63 }64
65 public static void readObject() throwsFileNotFoundException, IOException, ClassNotFoundException {66 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.txt"))) {67 Person p =(Person)ois.readObject();68 System.out.println("name: "+p.getName()+", age: "+p.getAge());69 }70 }71
72 public static void writeTeacher() throwsFileNotFoundException, IOException, ClassNotFoundException {73 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {74 Person per = new Person("孙悟空" , 500);75 Teacher t1 = new Teacher("唐僧", per);76 Teacher t2 = new Teacher("菩提祖师", per);77
78 //下面四行只会序列化三个对象,其中t1, t2 将引用同一个对象
79 oos.writeObject(t1);80 oos.writeObject(t2);81 oos.writeObject(per);82 oos.writeObject(t2);83 } catch(IOException e) {84 e.printStackTrace();85 }86
87 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"))) {88 //反序列化需要按照序列化的顺序取对象
89 Teacher t1 =(Teacher)ois.readObject();90 Teacher t2 =(Teacher)ois.readObject();91 Person p =(Person)ois.readObject();92 Teacher t3 =(Teacher)ois.readObject();93 System.out.println("t1的student引用和p是否相同: "+ (t1.getStudent() ==p));94 System.out.println("t2的student引用和p是否相同: "+ (t2.getStudent() ==p));95 System.out.println("t2和t3是否是同一个对象: "+ (t2 ==t3));96 } catch(IOException e) {97 e.printStackTrace();98 }99 }100
101 public static void mutable() throwsFileNotFoundException, IOException, ClassNotFoundException {102 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("mutable.txt"));103 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("mutable.txt"))) {104 Person per = new Person("孙悟空" , 500);105 oos.writeObject(per);106 per.setName("猪八戒");107 //per在前面已经序列化过,这里再序列化不会生效
108 oos.writeObject(per);109 Person p1 =(Person)ois.readObject();110 Person p2 =(Person)ois.readObject();111 System.out.println(p1==p2);112 System.out.println(p2.getName());113 } catch(IOException e) {114 e.printStackTrace();115 }116 }117
118 public static void main(String[] args) throwsFileNotFoundException, ClassNotFoundException, IOException {119 //writeObject();120 //不会调用构造器121 //readObject();122 //writeTeacher();
123 mutable();124 }125 }
View Code
自定义序列化
transient关键字
如果不希望序列化某个变量(例如银行账户信息等敏感信息),又或者某个实例变量类型不可序列化(例如某个成员变量是引用了一个不可序列化的类型),
我们不希望程序递归地去序列化这些对象,则可以通过transient关键字来修饰变量,程序就不会序列化这个变量,例如下面这样。
1 class Person implementsjava.io.Serializable {2 ...3 privateString name;4 private transient intage;5 ...
如果用上面的例子序列化这个类,再执行程序将会得到下面的结果,可以看到aga并没有取序列化之前的结果,而是被初始化成0了
1 有参数构造器2 name: 孙悟空, age: 0
通过重写writeObject/readObject自定义序列化
transient可以对某些变量进行屏蔽序列化效果,但是过于死板。Java还提供了一种方法来自定义序列化,就是在目标类中重写流对象的writeObject/readObject方法。
上面的序列化方式都使用的是流对象的默认序列化方法,oos.defaultWriteObject/ois.defaultReadObject,包括transient关键字,默认就是不做序列化。
而通过重写这两个方法,可以很具体地对目标类的指定变量做特殊操作,transient关键字修饰的变量不再是直接屏蔽,而是和其他变量一样可以自定义序列化的方式。
还有一个可重写的方法是readObjectNoData(),当序列化不完整(例如序列化版本不同,或者序列化流被篡改)时使用这个方法可以正确地初始化反序列化的对象。
重写的writeObject跟重写readObject总是成对出现,在序列化中做了什么操作(例如加密),在反序列化中需要做相反操作,且各个变量序列化和反序列化的顺序需要一样。
例如下面这样,
1 class Person implementsjava.io.Serializable {2
3 privateString name;4 private transient intage;5
6 public Person(String name, intage) {7 System.out.println("有参数构造器");8 this.name =name;9 this.age =age;10 }11
12 private void writeObject(java.io.ObjectOutputStream oos) throwsIOException {13 oos.writeObject(newStringBuilder(name).reverse());14 oos.writeInt(age);15 }16
17 private void readObject(java.io.ObjectInputStream ois) throwsIOException, ClassNotFoundException {18 this.name =((StringBuilder)ois.readObject()).reverse().toString();19 this.age =ois.readInt();20 }21 ...22 }
执行下面测试类,
1 public classObjectIO {2 public static voidwriteObject() {3 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.txt"))) {4 Person per = new Person ("孙悟空",500);5 oos.writeObject(per);6 } catch(IOException e) {7 e.printStackTrace();8 }9 }10
11 public static void readObject() throwsFileNotFoundException, IOException, ClassNotFoundException {12 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.txt"))) {13 Person p =(Person)ois.readObject();14 System.out.println("name: "+p.getName()+", age: "+p.getAge());15 }16 }17
18
19 public static void main(String[] args) throwsFileNotFoundException, ClassNotFoundException, IOException {20 writeObject();21 //不会调用构造器
22 readObject();23 }24 }
测试类中,序列化时对name进行了反转,相当于加密,即使在传输过程中被截获,也是加密的结果。 在反序列化过程中,对name进行了相反操作。
而对于age,虽然有transient关键字修饰,但是执行了通过重写writeObject方法,依然可以自定义这个变量的序列化方式;在反序列化中依然取得了结果。
执行结果,
1 有参数构造器2 name: 孙悟空, age: 500
通过重写writeReplace自定义序列化
Java在序列化某个对象之前,将先调用writeReplace(), writeReplace()方法的返回值将用来替换实际将被序列化的对象,例如下面,
1 packageio;2
3 importjava.io.FileInputStream;4 importjava.io.FileNotFoundException;5 importjava.io.FileOutputStream;6 importjava.io.IOException;7 importjava.io.ObjectInputStream;8 importjava.io.ObjectOutputStream;9 importjava.io.ObjectStreamException;10 importjava.util.ArrayList;11
12
13 class Person implementsjava.io.Serializable {14 publicString getName() {15 returnname;16 }17 public voidsetName(String name) {18 this.name =name;19 }20 public intgetAge() {21 returnage;22 }23 public void setAge(intage) {24 this.age =age;25 }26 privateString name;27 private transient intage;28
29 public Person(String name, intage) {30 System.out.println("有参数构造器");31 this.name =name;32 this.age =age;33 }34
35 private void writeObject(java.io.ObjectOutputStream oos) throwsIOException {36 oos.writeObject(newStringBuilder(name).reverse());37 oos.writeInt(age);38 }39
40 private void readObject(java.io.ObjectInputStream ois) throwsIOException, ClassNotFoundException {41 this.name =((StringBuilder)ois.readObject()).reverse().toString();42 this.age =ois.readInt();43 }44
45 private Object writeReplace() throwsObjectStreamException {46 ArrayList list = new ArrayList();47 list.add(name);48 list.add(age);49 returnlist;50 }51 }52
53
54
55 public classObjectIO {56
57 public static void writeReplaceTest() throwsFileNotFoundException, IOException {58 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("writeReplace.txt"));59 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("writeReplace.txt"))) {60 Person per = new Person("孙悟空", 500);61 oos.writeObject(per);62 ArrayList list =(ArrayList)ois.readObject();63 System.out.println(list);64 } catch(Exception e) {65 e.printStackTrace();66 }67 }68
69 public static void main(String[] args) throwsFileNotFoundException, ClassNotFoundException, IOException {70 writeReplaceTest();71 }72 }
View Code
可以看到,我们序列化的是Person对象,但是我们取出的是list对象;
我们同时重写了writeObject和writeReplace,最后反序列化的结果由writeReplace返回的对象替代了。执行结果如下,
1 有参数构造器2 [孙悟空, 500]
通过重写readResolve自定义序列化
与writeReplace对应的是readResolve方法,它将紧接着readObject调用,readObject的结果将被直接丢弃,readResolve的返回值将替代readObject的返回值。
对象序列化会破坏单例设计,因为在反序列化过程中,将会生成一个新的对象并用原来序列化时候的值来初始化,也就是反序列化和序列化两个对象不是同一个对象。
但是对于单例类,例如枚举类,如果反序列化得到的对象不是序列化之前的对象,这将违背单例类的原则,因此我们需要用一个特殊的反序列方法readResolve().
值得注意的是,readResolve()跟writeReplace()一样可以使用任意访问控制符,这样子类就有可能继承父类的readResolve()方法。这将会有一个问题,如果父类包含
一个protected或public的readResolve()方法时,如果子类不重写该方法,在反序列化时将会得到一个父类对象,这显然不是程序想要的结果,而且不易发现。
但是总是在子类中重写这个方法也挺麻烦,无疑增加了不必要的工作量,而且还容易忽视这前面的潜在问题。
因此如果一个类将成为父类的话,要么这个类是final类,则可以用任意访问权限修饰它的readResolve()方法; 要么将它的readResolve()方法最好是定义为private的。
readResolve()的作用就是在单例类的反序列化时,取代反序列化的结果, 例如下面的场景。
1 packageio;2
3 importjava.io.FileInputStream;4 importjava.io.FileNotFoundException;5 importjava.io.FileOutputStream;6 importjava.io.IOException;7 importjava.io.ObjectInputStream;8 importjava.io.ObjectOutputStream;9
10 class Orientation implementsjava.io.Serializable {11 public static final Orientation HORIZONTAL = new Orientation(1);12 public static final Orientation VERTICAL = new Orientation(2);13 private intvalue;14 private Orientation(intvalue) {15 this.value =value;16 }17 }18
19 public classReadResolveTest {20 public static void main(String[] args) throwsFileNotFoundException, IOException, ClassNotFoundException {21 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("readResolve.txt"));22 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("readResolve.txt"))) {23 oos.writeObject(Orientation.HORIZONTAL);24 Orientation ori =(Orientation)ois.readObject();25 System.out.println(ori ==Orientation.HORIZONTAL);26 }27 }28 }
执行结果发现ori与Orientation.HORIZONTAL不相等,因为反序列化是生成新对象的过程(其实是一个拷贝对象的过程,因为这里的类初始化方法是private的)。
为了解决这个问题,我们在目标类中重写readResolve()方法,
1 packageio;2
3 importjava.io.FileInputStream;4 importjava.io.FileNotFoundException;5 importjava.io.FileOutputStream;6 importjava.io.IOException;7 importjava.io.ObjectInputStream;8 importjava.io.ObjectOutputStream;9 importjava.io.ObjectStreamException;10
11 class Orientation implementsjava.io.Serializable {12 public static final Orientation HORIZONTAL = new Orientation(1);13 public static final Orientation VERTICAL = new Orientation(2);14 private intvalue;15 public Orientation(intvalue) {16 this.setValue(value);17 }18 public intgetValue() {19 returnvalue;20 }21 public void setValue(intvalue) {22 this.value =value;23 }24 private Object readResolve() throwsObjectStreamException {25 if (value==1) {26 returnHORIZONTAL;27 }28
29 if (value==2) {30 returnVERTICAL;31 }32
33 return null;34 }35 }36
37 public classReadResolveTest {38 public static void main(String[] args) throwsFileNotFoundException, IOException, ClassNotFoundException {39 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("readResolve.txt"));40 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("readResolve.txt"))) {41
42 oos.writeObject(Orientation.HORIZONTAL);43
44 //紧接着readObject(),将会调用重写的readResolve(),并取代readObject45 //我们在readResolve()中返回的是单例,这就解决对象序列化破坏单例设计模型的问题了
46 Orientation ori =(Orientation)ois.readObject();47 System.out.println(ori==Orientation.HORIZONTAL);48 }49 }50 }
再来执行,发现反序列化就可以得到序列化的同一个对象了。执行结果为true.
使用Externalizable接口实现序列化
这种方式与实现Serializable接口方式基本相同,但是Externalizable必须要在目标类中手工实现两个方法:writeExternal 用来实现序列化,用来实现反序列化。 且目标类必须包含一个默认构造函数(无参).
下面是一个例子,注意在writeExternal和readExternal使用的是ObjectOutput和ObjectInput而不是ObjectInputStream和ObjectOutputStream.
1 packageio;2
3 importjava.io.FileInputStream;4 importjava.io.FileNotFoundException;5 importjava.io.FileOutputStream;6 importjava.io.IOException;7 importjava.io.ObjectInput;8 importjava.io.ObjectInputStream;9 importjava.io.ObjectOutput;10 importjava.io.ObjectOutputStream;11
12 class People implementsjava.io.Externalizable {13 publicString getName() {14 returnname;15 }16 public voidsetName(String name) {17 this.name =name;18 }19 public intgetAge() {20 returnage;21 }22 public void setAge(intage) {23 this.age =age;24 }25 privateString name;26 private transient intage;27
28 public People(String name, intage) {29 System.out.println("有参数构造器");30 this.name =name;31 this.age =age;32 }33
34 publicPeople() {35 System.out.println("无参数构造器");36 }37
38 @Override39 public void writeExternal(java.io.ObjectOutput out) throwsIOException {40 out.writeObject(newStringBuffer(name).reverse());41 out.writeInt(age);42 }43
44 @Override45 public void readExternal(java.io.ObjectInput in) throwsIOException, ClassNotFoundException {46 this.name =((StringBuffer)in.readObject()).reverse().toString();47 this.age =in.readInt();48 }49 }50
51 public classExternalizableTest {52 public static void main(String[] args) throwsFileNotFoundException, IOException, ClassNotFoundException {53 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("external.txt"));54 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("external.txt"))) {55 People peo = new People("孙悟空" , 500);56 oos.writeObject(peo);57
58 People p =(People)ois.readObject();59 System.out.println(p.getName()+" : "+p.getAge());60 } catch(Exception e) {61 e.printStackTrace();62 }63 }64 }
执行结果, 可以看到在反序列化过程中调用了默认构造函数
1 有参数构造器2 无参数构造器3 孙悟空 : 500
反序列化兼容—— class版本
在反序列化时必须提供class文件,但class文件随着程序升级也会有不同的版本,但很多情况下需要保证两个class版本序列化能兼容,该如何做到呢?
Java的序列化机制允许定义一个version值,只要升级前后两个class的version值一样,就看作同一个版本,序列化前后能兼容。
定义如下,
1 public classTest {2 ...3 private static final long serialVersionUID = 512L;4 ...5 }
通常建议在每个类中都加入这个变量,这样在在反序列化时,即使提供的class文件版本已经跟序列化时的class文件有所改动,但只要这个version变量不变,就可以反序列化。
如果不显式定义version变量,JVM会根据类信息计算出一个(通常会变化),从而导致反序列化因为类版本不同而失败。
通过JDK下的serialver.exe 可以获取该类的serverUID
命令: serialver Person
输出结果: Person: static final long serialVersionUID = -2595800114629327570L;