Java序列化和反序列化

序列化和反序列化的概念

把对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:
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。

参考链接:http://www.cnblogs.com/xdp-gacl/p/3777987.html

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 方法将旧类序列化成新版本。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值