博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java序列化和反序列化
阅读量:5877 次
发布时间:2019-06-19

本文共 15738 字,大约阅读时间需要 52 分钟。

序列化和反序列化的概念

把对象转换为字节序列的过程称为对象的序列化。

把字节序列恢复为对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:
1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
2) 在网络上传送对象的字节序列。

  在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

JDK类库中的序列化API


  java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。


  java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。


  只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以 采用默认的序列化方式 。

对象序列化包括如下步骤:

  1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;

  2) 通过对象输出流的writeObject()方法写对象。


  对象反序列化的步骤如下:

  1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;

  2) 通过对象输入流的readObject()方法读取对象。


对象序列化和反序列范例:

定义一个Person类:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
package map; import java.io.Serializable; /**  * Created by benjamin on 12/5/15.  */ public class Person implements Serializable{
private static final long serialVersionUID = -3751291995104363537L; private int age; private String name; private String sex; public int getAge() {
return age; } public void setAge(int age) {
this.age = age; } public String getName() {
return name; } public void setName(String name) {
this.name = name; } public String getSex() {
return sex; } public void setSex(String sex) {
this.sex = sex; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
package map; import java.io.*; /**  * Created by piqiu on 12/8/15.  */ public class SerializableTest {
private static final String DISK_PATH = "/Users/piqiu1/Person"; public static void main(String[] args) throws IOException, ClassNotFoundException {
Person p = new Person(); p.setName("benjamin"); p.setAge(24); p.setSex("man"); serializePerson(p); Person p2 = deserializePerson(); System.out.println("name: " + p2.getName() + ", age: " + p2.getAge() + ", sex: " + p2.getSex()); } private static void serializePerson(Person p) throws IOException {
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File(DISK_PATH))); oo.writeObject(p); System.out.println("Person序列化成功"); oo.close(); } private static Person deserializePerson() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File(DISK_PATH))); Person p = (Person)ois.readObject(); System.out.println("Person反序列化成功"); return p; } }

执行结果:

1 2 3
Person序列化成功 Person反序列化成功 name: benjamin, age: 24, sex: man

我们设定的路径下面也生成了序列化的文件。

serialVersionUID的作用


s​e​r​i​a​l​V​e​r​s​i​o​n​U​I​D​:​ ​字​面​意​思​上​是​序​列​化​的​版​本​号​,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量


private static final long serialVersionUID


一般如果在eclipse或者myeclipse中不加版本号会出现警告。

采用 
+Add default serial version ID
这种方式生成的serialVersionUID是1L,例如:


private static final long serialVersionUID = 1L;


采用
+Add generated serial version ID
这种方式生成的serialVersionUID是根据类名,接口名,方法和属性等来生成的,例如:


private static final long serialVersionUID = 4603642343377807741L;


添加了之后就不会出现警告了。


在IDEA编辑器中可能会有的人不加版本号也不会警告,这是因为配置的问题。


打开Preferences -> 搜索serialVersionUID -> 勾选serializable class without serialVersionUID -> Apply


这样就会出现提示了。


那么serialVersionUID(序列化版本号)到底有什么用呢?

如果不加serialVersionUID,也是可以正常进行序列化,但是如果以后项目中需要对实体类进行增减字段的话,再进行反序列化就会报错,错误为:


1 2 3 4
Exception in thread "main" java.io.InvalidClassException: Customer; 2 local class incompatible: 3 stream classdesc serialVersionUID = -88175599799432325, 4 local class serialVersionUID = -5182532647273106745


serialVersionUID的取值

  serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。

类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的 serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定serialVersionUID,为它赋予明确的值。

  显式地定义serialVersionUID有两种用途:

1、 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
2、 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
参考链接:

readObjectNoData的使用


实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。如果你接受了默认的序列化形式,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容。

如果你实现了一个带有实例域的类,它是可序列化和可扩展的,你就应该担心这样一条告诫。如果类有一些约束条件,当类的实例域被初始化成它们的默认值(整数类型为0,boolean为false,对象引用类型为null)时,就会违背这些约束条件,这时候你就必须给这个类添加这个readObjectNoData方法:

1 2 3
private void readObjectNoData() throws InvalidObjectException {
throw new InvalidObjectException("Stream data required"); }

下面举出一个例子来更好的理解何时和如何使用:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public class Person implements Serializable {
private static final long serialVersionUID = -1046907702282365423L; private int age; public Person(){} public int getAge() {
return age; } public void setAge(int age) {
this.age = age; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
public class SerializeTest {
private static final String fileUrl = "/Users/piqiu1/Desktop/seralize.txt"; public static void main(String[] args) {
write(); // read(); } private static void write() {
Person p = new Person(); p.setAge(10); ObjectOutputStream oos = null; try {
oos = new ObjectOutputStream(new FileOutputStream(new File(fileUrl))); oos.writeObject(p); oos.flush(); oos.close(); } catch (IOException e) {
e.printStackTrace(); } } private static void read() {
ObjectInputStream ois = null; try {
ois = new ObjectInputStream(new FileInputStream(new File(fileUrl))); Person p = (Person)ois.readObject(); System.out.println(p.getAge()); ois.close(); } catch (IOException e) {
e.printStackTrace(); } catch (ClassNotFoundException e) {
e.printStackTrace(); } } }

上面是一个简单的序列化和反序列化的例子,现在我们在原来Person类的基础上,继承一个类Animals:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public class Animals implements Serializable {
private static final long serialVersionUID = 2768202150914525915L; private String name; public Animals(){} public String getName() {
return name; } public void setName(String name) {
this.name = name; } }

同时让Person继承它

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public class Person extends Animals implements Serializable {
private static final long serialVersionUID = -1046907702282365423L; private int age; public Person(){} public int getAge() {
return age; } public void setAge(int age) {
this.age = age; } }
1 2 3 4 5 6 7 8 9 10 11 12 13
private static void read() {
ObjectInputStream ois = null; try {
ois = new ObjectInputStream(new FileInputStream(new File(fileUrl))); Person p = (Person)ois.readObject(); System.out.println(p.getAge() + " " + p.getName()); ois.close(); } catch (IOException e) {
e.printStackTrace(); } catch (ClassNotFoundException e) {
e.printStackTrace(); } }

上面read的时候输出的p.getName()为空。这里我们就要使用readObjectNoData方法了,在Animals类中加入下面的方法:

1 2 3
private void readObjectNoData() {
this.name = "benjamin"; }

这样再执行,就能够输出我们给设置的默认值了。

考虑使用自定义序列化形式


考虑以一个对象为根的对象图,相对于它的物理表示法而言,该对象的默认序列化形式是一种比较有效的编码形式。换句话说,默认的序列化形式描述了该对象内部所包含的数据,以及没一个可以从这个对象到达的其他对象的内部数据。它也描述了所有这些对象被连接起来后的拓扑结构。对于一个对象来说,理想的序列化形式应该只包含该对象锁表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的。

如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。

1 2 3 4 5 6 7 8 9 10
public class Name implements Serializable {
private final String lastName; private final String firstName; private final String middleName; ...// }

从逻辑的角度而言,一个名字包含三个字符串,分别代表姓、名和中间名。Name中的实例域精确地反映了它的逻辑内容。

下面的例子与Name不同,它是另一个极端,该类表示了一个字符串列表:

1 2 3 4 5 6 7 8 9 10 11 12
public class StringList implements Serializable {
private int size = 0; private Entry head = null; private static class Entry implements Serializable {
String data; Entry next; Entry previous; } ... // }

当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下4个缺点:

1、它使这个类的导出API永远地束缚在该类的内部表示法上。
2、它会消耗过多的空间
3、它会消耗过多的时间:序列化逻辑并不了解对象图的拓扑关系,所以它必须要经过一个昂贵的图遍历(traversal)过程。在上面的例子中,沿着next引用进行遍历是非常简单的。
4、它会引起栈溢出:默认的序列化过程要对对象图执行一次递归遍历,即使对于中等规模的对象图,这样的操作也可能会引起栈溢出。在我的机器上,如果StringList实例包含1258个元素,对它进行序列化就会导致栈溢出。到底多少个元素就会引发栈溢出,这要取决于JVM的具体实现以及Java启动时的命令行参数,(比如Heap Size的-Xms与-Xmx的值)有些实现可能根本不存在这样的问题

下面我们使用了writeObject和readObject方法和transient修饰符来改变了这个方法的实现(transient修饰符表明这个实例域将从一个类的默认序列化形式中省略掉)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
public class StringList implements Serializable {
private transient int size = 0; private transient Entry head = null; private static class Entry {
String data; Entry next; Entry previous; } public final void add(String s) {...} private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject(); s.writeInt(size); //以正确的顺序写出所有元素 for (Entry e = head; e != null; e = e.next) s.writeObject(e.data); } private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject(); int numElements = s.readInt(); for (int i = 0; i < numElements; i++) add((String) s.readObject()); } }

我们还可以利用writeObject和readObject来进行模糊化序列化数据的操作。

假设我们有一个Person类要进行序列化操作,但是里面有一个age字段是敏感数据,毕竟女士忌谈年龄。我们可以在序列化之前模糊化该数据,将数位循环左移一位,然后在反序列化之后复位。(你可以用更安全的算法代替)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
package Effective; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /**  * Created by piqiu on 2/5/16.  */ public class Person extends Animals implements Serializable {
private static final long serialVersionUID = -1046907702282365423L; private String firstName; private String lastName; private int age; private Person spouse; public Person(String fn, String ln, int a) {
this.firstName = fn; this.lastName = ln; this.age = a; } private void writeObject(ObjectOutputStream stream) throws IOException {
// 加密数据 age = age << 2; stream.defaultWriteObject(); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject(); // 解密数据 age = age << 2; } @Override public String toString() {
return "[Person: firstName=" + firstName + " lastName=" + lastName + " age=" + age + " spouse=" + (spouse != null ? spouse.getFirstName() : "[null]") + "]"; } public String getFirstName() {
return firstName; } public void setFirstName(String firstName) {
this.firstName = firstName; } public String getLastName() {
return lastName; } public void setLastName(String lastName) {
this.lastName = lastName; } public int getAge() {
return age; } public void setAge(int age) {
this.age = age; } public Person getSpouse() {
return spouse; } public void setSpouse(Person spouse) {
this.spouse = spouse; } }

writeReplace和readResolve的使用


序列化非常强大,我们可以通过流把对象保存在磁盘上,再从磁盘上读取转为对象,但是如果单例的对象这么转换过后可就不是单例了,为了防止这种情况发生,我们可以在单例中加入readResolve这个方法来保证单例的可靠性。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
package Effective; import java.io.Serializable; /**  * Created by piqiu on 2/5/16.  */ public class MySingletion implements Serializable {
private static final long serialVersionUID = -2786296717146940199L; private MySingletion(){} private static final MySingletion instance = new MySingletion(); public static MySingletion getInstance() {
return instance; } private Object readResolve() {
return instance; } }

很多情况下,类中包含一个核心数据元素,通过它可以派生或找到类中的其他字段。在此情况下,没有必要序列化整个对象。可以将字段标记为 transient,但是每当有方法访问一个字段时,类仍然必须显式地产生代码来检查它是否被初始化。

如果首要问题是序列化,那么最好指定一个 flyweight 或代理放在流中。为原始 Person 提供一个 writeReplace 方法,可以序列化不同类型的对象来代替它。类似地,如果反序列化期间发现一个 readResolve 方法,那么将调用该方法,将替代对象提供给调用者。
writeReplace 和 readResolve 方法使 Person 类可以将它的所有数据(或其中的核心数据)打包到一个 PersonProxy 中,将它放入到一个流中,然后在反序列化时再进行解包。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
class PersonProxy     implements java.io.Serializable {
public PersonProxy(Person orig) {
data = orig.getFirstName() + "," + orig.getLastName() + "," + orig.getAge(); if (orig.getSpouse() != null) {
Person spouse = orig.getSpouse(); data = data + "," + spouse.getFirstName() + "," + spouse.getLastName() + "," + spouse.getAge(); } } public String data; private Object readResolve() throws java.io.ObjectStreamException {
String[] pieces = data.split(","); Person result = new Person(pieces[0], pieces[1], Integer.parseInt(pieces[2])); if (pieces.length > 3) {
result.setSpouse(new Person(pieces[3], pieces[4], Integer.parseInt (pieces[5]))); result.getSpouse().setSpouse(result); } return result; } } public class Person implements java.io.Serializable {
public Person(String fn, String ln, int a) {
this.firstName = fn; this.lastName = ln; this.age = a; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public Person getSpouse() { return spouse; } private Object writeReplace() throws java.io.ObjectStreamException {
return new PersonProxy(this); } public void setFirstName(String value) { firstName = value; } public void setLastName(String value) { lastName = value; } public void setAge(int value) { age = value; } public void setSpouse(Person value) { spouse = value; } public String toString() {
return "[Person: firstName=" + firstName + " lastName=" + lastName + " age=" + age + " spouse=" + spouse.getFirstName() + "]"; } private String firstName; private String lastName; private int age; private Person spouse; }

注意,PersonProxy 必须跟踪 Person 的所有数据。这通常意味着代理需要是 Person 的一个内部类,以便能访问 private 字段。有时候,代理还需要追踪其他对象引用并手动序列化它们,例如 Person 的 spouse。

这种技巧是少数几种不需要读/写平衡的技巧之一。例如,一个类被重构成另一种类型后的版本可以提供一个 readResolve 方法,以便静默地将被序列化的对象转换成新类型。类似地,它可以采用 writeReplace 方法将旧类序列化成新版本。

转载地址:http://ynuix.baihongyu.com/

你可能感兴趣的文章
Mind_Manager_2
查看>>
手动升级 Confluence - 规划你的升级
查看>>
汽车常识全面介绍 - 悬挂系统
查看>>
电子政务方向:We7.Cloud政府云门户
查看>>
虚拟机Centos7连接Internet
查看>>
ansible 基本操作(初试)
查看>>
更改tomcat的根目录路径
查看>>
51nod 1292 字符串中的最大值V2(后缀自动机)
查看>>
加快ALTER TABLE 操作速度
查看>>
学习笔记之软考数据库系统工程师教程(第一版)
查看>>
基本网络概念
查看>>
将 ASP.NET Core 2.0 项目升级至 ASP.NET Core 2.1 RC 1
查看>>
js提交图片转换为base64
查看>>
学习CodeIgniter框架之旅(二)继承自定义类
查看>>
Y2161 Hibernate第三次考试 2016年8月18日 试卷分析
查看>>
Angular CLI 使用教程指南参考
查看>>
PHP 程序员的技术成长规划
查看>>
用于守护进程的出错处理函数
查看>>
memcached 分布式聚类算法
查看>>
禁止body滚动允许div滚动防微信露底
查看>>