Java回顾之序列化
来源:互联网 发布:大数据和人工智能 编辑:程序博客网 时间:2024/06/05 06:01
在这篇文章里,我们关注对象序列化。
首先,我们来讨论一下什么是序列化以及序列化的原理;然后给出一个简单的示例来演示序列化和反序列化;有时有些信息是不应该被序列化的,我们应该如何控制;我们如何去自定义序列化内容;最后我们讨论一下在继承结构的场景中,序列化需要注意哪些内容。
序列化概述
序列化,简单来讲,就是以“流”的方式来保存对象,至于保存的目标地址,可以是文件,可以是数据库,也可以是网络,即通过网络将对象从一个节点传递到另一个节点。
我们知道在Java的I/O结构中,有ObjectOutputStream和ObjectInputStream,它们可以实现将对象输出为二进制流,并从二进制流中获取对象,那为什么还需要序列化呢?这需要从Java变量的存储结构谈起,我们知道对Java来说,基础类型存储在栈上,复杂类型(引用类型)存储在堆中,对于基础类型来说,上述的操作时可行的,但对复杂类型来说,上述操作过程中,可能会产生重复的对象,造成错误。
而序列化的工作流程如下:
1)通过输出流保存的对象都有一个唯一的序列号。
2)当一个对象需要保存时,先对其序列号进行检查。
3)当保存的对象中已包含该序列号时,不需要再次保存,否则,进入正常保存的流程。
正是通过序列号的机制,序列化才可以完整准确的保存对象的各个状态。
序列化保存的是对象中的各个属性的值,而不是方法或者方法签名之类的信息。对于方法或者方法签名,只要JVM能够找到正确的ClassLoader,那么就可以invoke方法。
序列化不会保存类的静态变量,因为静态变量是作用于类型,而序列化作用于对象。
简单的序列化示例
序列化的完整过程包括两部分:
1)使用ObjectOutputStream将对象保存为二进制流,这一步叫做“序列化”。
2)使用ObjectInputStream将二进制流转换成对象,这一步叫做“反序列化”。
下面我们来演示一个简单的示例,首先定义一个Person对象,它包含name和age两个信息。
定义Person对象class Person implements Serializable{ private String name; private int age; public void setName(String name) { this.name = name; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public int getAge() { return age; } public String toString() { return "Name:" + name + "; Age:" + age; }}然后是两个公共方法,用来完成读、写对象的操作:
private static void writeObject(Object obj, String filePath){ try { FileOutputStream fos = new FileOutputStream(filePath); ObjectOutputStream os = new ObjectOutputStream(fos); os.writeObject(obj); os.flush(); fos.flush(); os.close(); fos.close(); System.out.println("序列化成功。"); } catch(Exception ex) { ex.printStackTrace(); }}private static Object readObject(String filePath){ try { FileInputStream fis = new FileInputStream(filePath); ObjectInputStream is = new ObjectInputStream(fis); Object temp = is.readObject(); fis.close(); is.close(); if (temp != null) { System.out.println("反序列化成功。"); return temp; } } catch(Exception ex) { ex.printStackTrace(); } return null;}
这里,我们将对象保存的二进制流输出到磁盘文件中。
接下来,我们首先来看“序列化”的方法:
private static void serializeTest1(){ Person person = new Person(); person.setName("Zhang San"); person.setAge(30); System.out.println(person); writeObject(person, "d:\\temp\\test\\person.obj");}
我们定义了一个Person实例,然后将其保存到d:\temp\test\person.obj中。
最后,是“反序列化”的方法:
private static void deserializeTest1(){ Person temp = (Person)readObject("d:\\temp\\test\\person.obj"); if (temp != null) { System.out.println(temp); }}
它从d:\temp\test\person.obj中读取对象,然后进行输出。
上述两个方法的执行结果如下:
Name:Zhang San; Age:30序列化成功。反序列化成功。Name:Zhang San; Age:30
可以看出,读取的对象和保存的对象是完全一致的。
隐藏非序列化信息
有时,我们的业务对象中会包含很多属性,而有些属性是比较隐私的,例如年龄、银行卡号等,这些信息是不太适合进行序列化的,特别是在需要通过网络来传输对象信息时,这些敏感信息很容易被窃取。
Java使用transient关键字来处理这种情况,针对那些敏感的属性,我们只需使用该关键字进行修饰,那么在序列化时,对应的属性值就不会被保存。
我们还是看一个实例,这次我们定义一个新的Person2,其中age信息是我们不希望序列化的:
定义Person2对象class Person2 implements Serializable{ private String name; private transient int age; public void setName(String name) { this.name = name; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public int getAge() { return age; } public String toString() { return "Name:" + name + "; Age:" + age; }}
private transient int age;下面是“序列化”和“反序列化”的方法:
private static void serializeTest2(){ Person2 person = new Person2(); person.setName("Zhang San"); person.setAge(30); System.out.println(person); writeObject(person, "d:\\temp\\test\\person2.obj");}private static void deserializeTest2(){ Person2 temp = (Person2)readObject("d:\\temp\\test\\person2.obj"); if (temp != null) { System.out.println(temp); }}
它的输出结果如下:
Name:Zhang San; Age:30序列化成功。反序列化成功。Name:Zhang San; Age:0
可以看到经过反序列化的对象,age的信息变成了Integer的默认值0。
自定义序列化过程
我们可以对序列化的过程进行定制,进行更细粒度的控制。
思路是在业务模型中添加readObject和writeObject方法。下面看一个实例,我们新建一个类型,叫Person3:
class Person3 implements Serializable{ private String name; private transient int age; public void setName(String name) { this.name = name; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public int getAge() { return age; } public String toString() { return "Name:" + name + "; Age:" + age; } private void writeObject(ObjectOutputStream os) { try { os.defaultWriteObject(); os.writeObject(this.age); System.out.println(this); System.out.println("序列化成功。"); } catch(Exception ex) { ex.printStackTrace(); } } private void readObject(ObjectInputStream is) { try { is.defaultReadObject(); this.setAge(((Integer)is.readObject()).intValue() - 1); System.out.println("反序列化成功。"); System.out.println(this); } catch(Exception ex) { ex.printStackTrace(); } }}
请注意观察readObject和writeObject方法,它们都是private的,接受的参数是ObjectStream,然后在方法体内调用了defaultReadObject或者defaultWriteObject方法。
这里age同样是transient的,但是在保存对象的过程中,我们单独对其进行了保存,在读取时,我们将age信息读取出来,并进行了减1处理。
下面是测试方法:
private static void serializeTest3(){ Person3 person = new Person3(); person.setName("Zhang San"); person.setAge(30); System.out.println(person); try { FileOutputStream fos = new FileOutputStream("d:\\temp\\test\\person3.obj"); ObjectOutputStream os = new ObjectOutputStream(fos); os.writeObject(person); fos.close(); os.close(); } catch(Exception ex) { ex.printStackTrace(); }}private static void deserializeTest3(){ try { FileInputStream fis = new FileInputStream("d:\\temp\\test\\person3.obj"); ObjectInputStream is = new ObjectInputStream(fis); is.readObject(); fis.close(); is.close(); } catch(Exception ex) { ex.printStackTrace(); }}
输出结果如下:
Name:Zhang San; Age:30序列化成功。反序列化成功。Name:Zhang San; Age:29
可以看到,经过反序列化得到的对象,其age属性已经减1。
探讨serialVersionUID
在上文中,我们描述序列化原理时,曾经提及每个对象都会有一个唯一的序列号,这个序列号,就是serialVersionUID。
当我们的对象实现Serializable接口时,该接口可以为我们生成serialVersionUID。
有两种方式来生成serialVersionUID,一种是固定值:1L,一种是经过JVM计算,不同的JVM采取的计算算法可能不同。
下面就是两个serialVersionUID的示例:
private static final long serialVersionUID = 1L;private static final long serialVersionUID = -2380764581294638541L;
第一行是采用固定值生成的;第二行是JVM经过计算得出的。
那么serialVersionUID还有其他用途吗?
我们可以使用它来控制版本兼容。如果采用JVM生成的方式,我们可以看到,当我们业务对象的代码保持不变时,多次生成的serialVersionUID也是不变的,当我们对属性进行修改时,重新生成的serialVersionUID会发生变化,当我们对方法进行修改时,serialVersionUID不变。这也从另一个侧面说明,序列化是作用于对象属性上的。
当我们先定义了业务对象,然后对其示例进行了“序列化”,这时根据业务需求,我们修改了业务对象,那么之前“序列化”后的内容还能经过“反序列化”返回到系统中吗?这取决于业务对象是否定义了serialVersionUID,如果定义了,那么是可以返回的,如果没有定义,会抛出异常。
来看下面的示例,定义新的类型Person4:
class Person4 implements Serializable{ private String name; private int age; public void setName(String name) { this.name = name; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public int getAge() { return age; } private void xxx(){} public String toString() { return "Name:" + name + "; Age:" + age; }}
然后运行下面的方法:
private static void serializeTest4(){ Person4 person = new Person4(); person.setName("Zhang San"); person.setAge(30); writeObject(person, "d:\\temp\\test\\person4.obj");}
接下来修改Person4,追加address属性:
class Person4 implements Serializable{ private String name; private int age; private String address; public void setName(String name) { this.name = name; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public int getAge() { return age; } private void xxx(){} public String toString() { return "Name:" + name + "; Age:" + age; } public void setAddress(String address) { this.address = address; } public String getAddress() { return address; }}
然后运行“反序列化”方法:
private static void deserializeTest4(){ Person4 temp = (Person4)readObject("d:\\temp\\test\\person4.obj"); if (temp != null) { System.out.println(temp); }}
可以看到,运行结果如下:
java.io.InvalidClassException: sample.serialization.Person4; local class incompatible: stream classdesc serialVersionUID = -2380764581294638541, local class serialVersionUID = -473458100724786987 at java.io.ObjectStreamClass.initNonProxy(Unknown Source) at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source) at java.io.ObjectInputStream.readClassDesc(Unknown Source) at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source) at java.io.ObjectInputStream.readObject0(Unknown Source) at java.io.ObjectInputStream.readObject(Unknown Source) at sample.serialization.Sample.readObject(Sample.java:158) at sample.serialization.Sample.deserializeTest4(Sample.java:105) at sample.serialization.Sample.main(Sample.java:16)
但是当我们在Person4中添加serialVersionUID后,再次执行上述各步骤,得出的运行结果如下:
反序列化成功。Name:Zhang San; Age:30
有继承结构的序列化
业务对象会产生继承,这在管理系统中是经常看到的,如果我们有下面的业务对象:
class Person5{ private String name; private int age; public void setName(String name) { this.name = name; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public int getAge() { return age; } public String toString() { return "Name:" + name + "; Age:" + age; } public Person5(String name, int age) { this.name = name; this.age = age; }}class Employee extends Person5 implements Serializable{ public Employee(String name, int age) { super(name, age); } private String companyName; public void setCompanyName(String companyName) { this.companyName = companyName; } public String getCompanyName() { return companyName; } public String toString() { return "Name:" + super.getName() + "; Age:" + super.getAge() + "; Company:" + this.companyName; }}
Employee继承在Person5,Employee实现了Serializable接口,Person5没有实现,那么运行下面的方法:
private static void serializeTest5(){ Employee emp = new Employee("Zhang San", 30); emp.setCompanyName("XXX"); writeObject(emp, "d:\\temp\\test\\employee.obj");}private static void deserializeTest5(){ Employee temp = (Employee)readObject("d:\\temp\\test\\employee.obj"); if (temp != null) { System.out.println(temp); }}
会正常运行吗?事实上不会,它会抛出如下异常:
java.io.InvalidClassException: sample.serialization.Employee; no valid constructor at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source) at java.io.ObjectStreamClass.checkDeserialize(Unknown Source) at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source) at java.io.ObjectInputStream.readObject0(Unknown Source) at java.io.ObjectInputStream.readObject(Unknown Source) at sample.serialization.Sample.readObject(Sample.java:158) at sample.serialization.Sample.deserializeTest5(Sample.java:123) at sample.serialization.Sample.main(Sample.java:18)
原因:在有继承层次的业务对象,进行序列化时,如果父类没有实现Serializable接口,那么父类必须提供默认构造函数。
我们为Person5添加如下默认构造函数:
public Person5() { this.name = "Test"; this.age = 1; }
再次运行上述代码,结果如下:
Name:Zhang San; Age:30; Company:XXX序列化成功。反序列化成功。Name:Test; Age:1; Company:XXX
可以看到,反序列化后的结果,父类中的属性,已经被父类构造函数中的赋值代替了!
因此,我们推荐在有继承层次的业务对象进行序列化时,父类也应该实现Serializable接口。我们对Person5进行修改,使其实现Serializable接口,执行结果如下:
Name:Zhang San; Age:30; Company:XXX序列化成功。反序列化成功。Name:Zhang San; Age:30; Company:XXX这正是我们期望的结果。
- Java回顾之序列化
- Java回顾之序列化
- Java回顾之序列化
- Java序列化基础事项回顾
- Java基础回顾 : 对象序列化和反序列化
- Java之序列化
- Java之序列化
- java之序列化
- java之序列化
- Java之序列化
- java之序列化
- Java之序列化
- JAVA之序列化
- Java回顾之JDBC
- Java回顾之集合
- Java回顾之集合
- Java回顾之反射
- Java回顾之JDBC
- Codeforces Round #378 (Div. 2) C.Epidemic in Monstropolis (分块处理)
- Google浏览器Chrome安装失败,错误代码0xa0430721解决办法
- 面向对象程序设计-------c++继承小结
- 第九周-OJ-D等比数列
- cocos2d实例学习《learn cocos2d demos》
- Java回顾之序列化
- 求两个数的二进制数的对应位有多少位不同?
- ios webview自适应实际内容高度4种方法
- Unity 剔除和深度测试
- Linux下安装jdk
- HDU5950 Recursive sequence 沈阳赛
- 单例模式——世上只有一个我,再无其它!
- OGG 故障排除
- Spring源码阅读之DefaultListableBeanFactory系列-SimpleAliasRegistry