您的位置:首页 > 编程语言 > Java开发

Java序列化的几种方式以及序列化的作用

2017-03-03 00:00 239 查看
学习Java的同学注意了!!!
学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:589809992我们一起学Java!

本文着重讲解一下Java序列化的相关内容。
如果对Java序列化感兴趣的同学可以研究一下。
一.Java序列化的作用
有的时候我们想要把一个Java对象变成字节流的形式传出去,有的时候我们想要从一个字节流中恢复一个Java对象。例如,有的时候我们想要
把一个Java对象写入到硬盘或者传输到网路上面的其它计算机,这时我们就需要自己去通过java把相应的对象写成转换成字节流。对于这种通用
的操作,我们为什么不使用统一的格式呢?没错,这里就出现了java的序列化的概念。在Java的OutputStream类下面的子类ObjectOutput-
Stream类就有对应的WriteObject(Objectobject)其中要求对应的object实现了java的序列化的接口。
为了更好的理解java序列化的应用,我举两个自己在开发项目中遇到的例子:
1)在使用tomcat开发JavaEE相关项目的时候,我们关闭tomcat后,相应的session中的对象就存储在了硬盘上,如果我们想要在tomcat重启的
时候能够从tomcat上面读取对应session中的内容,那么保存在session中的内容就必须实现相关的序列化操作。
2)如果我们使用的java对象要在分布式中使用或者在rmi远程调用的网络中使用的话,那么相关的对象必须实现java序列化接口。
亲爱的小伙伴,大概你已经了解了java序列化相关的作用,接下来们来看看如何实现java的序列化吧。~
二.实现java对象的序列化和反序列化。
Java对象的序列化有两种方式。
a.是相应的对象实现了序列化接口Serializable,这个使用的比较多,对于序列化接口Serializable接口是一个空的接口,它的主要作用就是
标识这个对象时可序列化的,jre对象在传输对象的时候会进行相关的封装。这里就不做过多的介绍了。
下面是一个实现序列化接口的Java序列化的例子:非常简单

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
package
com.shop.domain;

import
java.util.Date;

public
class
Article
implements
java.io.Serializable{


private
static
final
long
serialVersionUID=1L;


private
Integerid;


private
Stringtitle;
//文章标题


private
Stringcontent;
//文章内容


private
StringfaceIcon;
//表情图标


private
DatepostTime;
//文章发表的时间


private
StringipAddr;
//用户的ip




private
Userauthor;
//回复的用户




public
IntegergetId(){


return
id;


}


public
void
setId(Integerid){


this
.id=id;


}


public
StringgetTitle(){


return
title;


}


public
void
setTitle(Stringtitle){


this
.title=title;


}


public
StringgetContent(){


return
content;


}


public
void
setContent(Stringcontent){


this
.content=content;


}


public
StringgetFaceIcon(){


return
faceIcon;


}


public
void
setFaceIcon(StringfaceIcon){


this
.faceIcon=faceIcon;


}


public
DategetPostTime(){


return
postTime;


}


public
void
setPostTime(DatepostTime){


this
.postTime=postTime;


}


public
UsergetAuthor(){


return
author;


}


public
void
setAuthor(Userauthor){


this
.author=author;


}


public
StringgetIpAddr(){


return
ipAddr;


}


public
void
setIpAddr(StringipAddr){


this
.ipAddr=ipAddr;


}





}

  b.实现序列化的第二种方式为实现接口Externalizable,Externlizable的部分源代码如下:

1

2

3

4

5

6

7

8
*
@see
java.io.ObjectInput


*
@see
java.io.Serializable


*
@since
JDK1.
1


*/

public
interface
Externalizable
extends
java.io.Serializable{


/**


*Theobject
implements
thewriteExternalmethodtosaveitscontents


*bycallingthemethodsofDataOutput
for
itsprimitivevaluesor

没错,Externlizable接口继承了java的序列化接口,并增加了两个方法:
voidwriteExternal(ObjectOutputout)throwsIOException;
voidreadExternal(ObjectInputin)throwsIOException,ClassNotFoundException;
首先,我们在序列化对象的时候,由于这个类实现了Externalizable接口,在writeExternal()方法里定义了哪些属性可以序列化,
哪些不可以序列化,所以,对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理,然后在反序列的时候自动调
用readExternal()方法,根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在测试类接收,就完成了反序列。
所以说Exterinable的是Serializable的一个扩展。
为了更好的理解相关内容,请看下面的例子:

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

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141
package
com.xiaohao.test;

import
java.io.Externalizable;

import
java.io.FileInputStream;

import
java.io.FileNotFoundException;

import
java.io.FileOutputStream;

import
java.io.IOException;

import
java.io.ObjectInput;

import
java.io.ObjectInputStream;

import
java.io.ObjectOutput;

import
java.io.ObjectOutputStream;

import
java.text.SimpleDateFormat;

import
java.util.Date;

/**


*测试实体类


*@author小浩


*@创建日期2015-3-12


*/

class
Person
implements
Externalizable{


private
static
final
long
serialVersionUID=1L;<br>StringuserName;


Stringpassword;


Stringage;






public
Person(StringuserName,Stringpassword,Stringage){


super
();


this
.userName=userName;


this
.password=password;


this
.age=age;


}






public
Person(){


super
();


}


public
StringgetAge(){


return
age;


}


public
void
setAge(Stringage){


this
.age=age;


}


public
StringgetUserName(){


return
userName;


}


public
void
setUserName(StringuserName){


this
.userName=userName;


}


public
StringgetPassword(){


return
password;


}


public
void
setPassword(Stringpassword){


this
.password=password;


}




/**


*序列化操作的扩展类


*/


@Override


public
void
writeExternal(ObjectOutputout)
throws
IOException{


//增加一个新的对象


Datedate=
new
Date();


out.writeObject(userName);


out.writeObject(password);


out.writeObject(date);


}




/**


*反序列化的扩展类


*/


@Override


public
void
readExternal(ObjectInputin)
throws
IOException,


ClassNotFoundException{


//注意这里的接受顺序是有限制的哦,否则的话会出错的


//例如上面先write的是A对象的话,那么下面先接受的也一定是A对象...


userName=(String)in.readObject();


password=(String)in.readObject();


SimpleDateFormatsdf=
new
SimpleDateFormat(
"yyyy-MM-dd"
);


Datedate=(Date)in.readObject();


System.out.println(
"反序列化后的日期为:"
+sdf.format(date));




}


@Override


public
StringtoString(){


//注意这里的年龄是不会被序列化的,所以在反序列化的时候是读取不到数据的


return
"用户名:"
+userName+
"密码:"
+password+
"年龄:"
+age;


}

}

/**


*序列化和反序列化的相关操作类


*@author小浩


*@创建日期2015-3-12


*/

class
Operate{


/**


*序列化方法


*@throwsIOException


*@throwsFileNotFoundException


*/


public
void
serializable(Personperson)
throws
FileNotFoundException,IOException{


ObjectOutputStreamoutputStream=
new
ObjectOutputStream(
new
FileOutputStream(
"a.txt"
));


outputStream.writeObject(person);


}




/**


*反序列化的方法


*@throwsIOException


*@throwsFileNotFoundException


*@throwsClassNotFoundException


*/


public
PersondeSerializable()
throws
FileNotFoundException,IOException,ClassNotFoundException{


ObjectInputStreamois=
new
ObjectInputStream(
new
FileInputStream(
"a.txt"
));


return
(Person)ois.readObject();


}





}

/**


*测试实体主类


*@author小浩


*@创建日期2015-3-12


*/

public
class
Test{


public
static
void
main(String[]args)
throws
FileNotFoundException,IOException,ClassNotFoundException{


Operateoperate=
new
Operate();


Personperson=
new
Person(
"小浩"
,
"123456"
,
"20"
);


System.out.println(
"为序列化之前的相关数据如下:\n"
+person.toString());


operate.serializable(person);


PersonnewPerson=operate.deSerializable();


System.out.println(
"-------------------------------------------------------"
);


System.out.println(
"序列化之后的相关数据如下:\n"
+newPerson.toString());


}





}

首先,我们在序列化UserInfo对象的时候,由于这个类实现了Externalizable接口,在writeExternal()方法里定义了哪些属性可
以序列化,哪些不可以序列化,所以,对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理,然后在反序列
的时候自动调用readExternal()方法,根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在测试类接收,就完成了反
序列。

***对于实现Java的序列化接口需要注意一下几点:
1.java中的序列化时transient变量(这个关键字的作用就是告知JAVA我不可以被序列化)和静态变量不会被序列
化(下面是一个测试的例子)

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
import
java.io.*;

class
Student1
implements
Serializable{


private
static
final
long
serialVersionUID=1L;


private
Stringname;


private
transient
Stringpassword;


private
static
int
count=
0
;


public
Student1(Stringname,Stringpassword){


System.out.println(
"调用Student的带参的构造方法"
);


this
.name=name;


this
.password=password;


count++;


}


public
StringtoString(){


return
"人数:"
+count+
"姓名:"
+name+
"密码:"
+password;


}

}

public
class
ObjectSerTest1{


public
static
void
main(Stringargs[]){


try
{


FileOutputStreamfos=
new
FileOutputStream(
"test.obj"
);


ObjectOutputStreamoos=
new
ObjectOutputStream(fos);


Student1s1=
new
Student1(
"张三"
,
"12345"
);


Student1s2=
new
Student1(
"王五"
,
"54321"
);


oos.writeObject(s1);


oos.writeObject(s2);


oos.close();


FileInputStreamfis=
new
FileInputStream(
"test.obj"
);


ObjectInputStreamois=
new
ObjectInputStream(fis);


Student1s3=(Student1)ois.readObject();


Student1s4=(Student1)ois.readObject();


System.out.println(s3);


System.out.println(s4);


ois.close();


}
catch
(IOExceptione){


e.printStackTrace();


}
catch
(ClassNotFoundExceptione1){


e1.printStackTrace();


}


}

}

  

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
import
java.io.FileInputStream;

import
java.io.FileOutputStream;

import
java.io.IOException;

import
java.io.ObjectInputStream;

import
java.io.ObjectOutputStream;

public
class
Test{




public
static
void
main(Stringargs[]){




try
{


FileInputStreamfis=
new
FileInputStream(
"test.obj"
);


ObjectInputStreamois=
new
ObjectInputStream(fis);


Student1s3=(Student1)ois.readObject();


Student1s4=(Student1)ois.readObject();


System.out.println(s3);


System.out.println(s4);


ois.close();


}
catch
(IOExceptione){


e.printStackTrace();


}
catch
(ClassNotFoundExceptione1){


e1.printStackTrace();


}


}







}

  
2.也是最应该注意的,如果你先序列化对象A后序列化B,那么在反序列化的时候一定记着JAVA规定先读到的对象
是先被序列化的对象,不要先接收对象B,那样会报错.尤其在使用上面的Externalizable的时候一定要注意读取
的先后顺序。
3.实现序列化接口的对象并不强制声明唯一的serialVersionUID,是否声明serialVersionUID对于对象序列化的向
上向下的兼容性有很大的影响。我们来做个测试:
思路一
把User中的serialVersionUID去掉,序列化保存。反序列化的时候,增加或减少个字段,看是否成功。

Java代码

1

2

3

4

5

6

7

8

9

10

11
public
class
User
implements
Serializable{

private
Stringname;


private
int
age;

private
long
phone;

private
List<UserVo>friends;

...<br>}

  

保存到文件中:

1

2

3

4

5

6

7

8

9

10

11
Java代码

ByteArrayOutputStreambos=
new
ByteArrayOutputStream();

ObjectOutputStreamos=
new
ObjectOutputStream(bos);

os.writeObject(src);

os.flush();

os.close();

byte
[]b=bos.toByteArray();

bos.close();

FileOutputStreamfos=
new
FileOutputStream(dataFile);

fos.write(b);

fos.close();

 

增加或者减少字段后,从文件中读出来,反序列化:

1

2

3

4

5

6

7

8

9

10

11
Java代码

ByteArrayOutputStreambos=
new
ByteArrayOutputStream();

ObjectOutputStreamos=
new
ObjectOutputStream(bos);

os.writeObject(src);

os.flush();

os.close();

byte
[]b=bos.toByteArray();

bos.close();

FileOutputStreamfos=
new
FileOutputStream(dataFile);

fos.write(b);

fos.close();

  

结果:抛出异常信息
Java代码

1

2

3

4

5

6

7

8
Exceptioninthread
"main"
java.io.InvalidClassException:serialize.obj.UserVo;local
class
incompatible:streamclassdescserialVersionUID=
3305402508581390189
,local
class
serialVersionUID=
7174371419787432394
atjava.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:
560
)

atjava.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:
1582
)

atjava.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:
1495
)

atjava.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:
1731
)

atjava.io.ObjectInputStream.readObject0(ObjectInputStream.java:
1328
)

atjava.io.ObjectInputStream.readObject(ObjectInputStream.java:
350
)

atserialize.obj.ObjectSerialize.read(ObjectSerialize.java:
74
)

atserialize.obj.ObjectSerialize.main(ObjectSerialize.java:
27
)

  

思路二
eclipse指定生成一个serialVersionUID,序列化保存,修改字段后反序列化
略去代码
结果:反序列化成功
结论
如果没有明确指定serialVersionUID,序列化的时候会根据字段和特定的算法生成一个serialVersionUID,当属性有变化时这个id发生了变化,所以反序列化的时候
就会失败。抛出“本地classd的唯一id和流中class的唯一id不匹配”。
jdk文档关于serialVersionUID的描述:
写道

如果可序列化类未显式声明serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认serialVersionUID值,如“Java(TM)对象序列化规范”中所述。不过,
强烈建议所有可序列化类都显式声明serialVersionUID值,原因是计算默认的serialVersionUID对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的InvalidClassException。因此,为保证serialVersionUID值跨不同java编译器实现的一致性,序列化类必须声明一个明确的serialVersionUID值。还强烈建议使用private修饰符显示声明serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类--serialVersionUID字段作为继承成员没有用处。数组类不能声明一个明确的serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配serialVersionUID值的要求。

三.实现序列化的其它方式(这是一个扩展内容,感兴趣的可以扩展一下)
1)是把对象包装成JSON字符串传输。
这里采用JSON格式同时使用采用Google的gson-2.2.2.jar进行转义
2)采用谷歌的ProtoBuf
随着Google工具protoBuf的开源,protobuf也是个不错的选择。对JSON,ObjectSerialize(Java的序列化和反序列化),
ProtoBuf做个对比。
定义一个通用的待传输的对象UserVo:

1

2

3

4

5

6

7

8

9
public
class
User

private
static
final
long
serialVersionUID=-5726374138698742258L;

{
private
Stringname;


private
int
age;


private
long
phone;


private
List<user>friends;


...set和get方法


}

</user>

 

初始化User的实例src:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16
Java代码

Useruser1=
new
UserVo();

user1.setName(
"user1"
);


user1.setAge(
30
);


user1.setPhone(13789126278L);


UserVof1=
new
UserVo();


f1.setName(
"tmac"
);


f1.setAge(
32
);


f1.setPhone(123L);


Useruser2=
new
User();


user2.setName(
"user2"
);


user2.setAge(
29
);


user2.setPhone(123L);<br>List<User>friends=
new
ArrayList<User>();


friends.add(user1);


friends.add(user2);


user1.setFriends(friends);

1.首先使用JOSN来实现序列化。

1

2
Java代码

Gsongson=
new
Gson();<br>Stringjson=gson.toJson(src);

  

得到的字符串:

1

2

3
Js代码

{
"name"
:
"user1"
,
"age"
:
30
,
"phone"
:
123
,
"friends"
:[{
"name"
:
"user1"
,
"age"
:
32
,
"phone"
:
123
},{
"name"
:
"user2"
,
"age"
:
29
,
"phone"
:
123
}]}

字节数为153
Json的优点:明文结构一目了然,可以跨语言,属性的增加减少对解析端影响较小。缺点:字节数过多,依赖于不同的第三方类库。

ObjectSerialize(Java的序列化和反序列化)

UserVo实现Serializalbe接口,提供唯一的版本号:
序列化方法:

Java代码

ByteArrayOutputStreambos=newByteArrayOutputStream();

ObjectOutputStreamos=newObjectOutputStream(bos);

os.writeObject(src);

os.flush();

os.close();

byte[]b=bos.toByteArray();

bos.close();

字节数是238

反序列化:

Java代码

ObjectInputStreamois=newObjectInputStream(fis);

vo=(UserVo)ois.readObject();

ois.close();

fis.close();

ObjectSerializalbe优点:java原生支持,不需要提供第三方的类库,使用比较简单。
缺点:无法跨语言,字节数占用比较大,某些情况下对于对象属性的变化比较敏感。
对象在进行序列化和反序列化的时候,必须实现Serializable接口,但并不强制声明唯一的serialVersionUID
是否声明serialVersionUID对于对象序列化的向上向下的兼容性有很大的影响。

GoogleProtoBuf

protocolbuffers是google内部得一种传输协议,目前项目已经开源(http://code.google.com/p/protobuf/)。
它定义了一种紧凑得可扩展得二进制协议格式,适合网络传输,并且针对多个语言有不同得版本可供选择。
以protobuf-2.5.0rc1为例,准备工作:
下载源码,解压,编译,安装

Shell代码

tarzxvfprotobuf-2.5.0rc1.tar.gz./configure./make./makeinstall

测试:

Shell代码

MacBook-Air:~ming$protoc--versionlibprotoc2.5.0

安装成功!
进入源码得java目录,用mvn工具编译生成所需得jar包,protobuf-java-2.5.0rc1.jar

1、编写.proto文件,命名UserVo.proto

1

2

3

4

5

6

7

8

9

10

11

12
Text代码

package
serialize;

optionjava_package=
"serialize"
;

optionjava_outer_classname=
"UserVoProtos"
;

messageUser{

optionalstringname=
1
;

optionalint32age=
2
;

optionalint64phone=
3
;

repeatedserialize.UserVofriends=
4
;

}

  

2、在命令行利用protoc工具生成builder类

Shell代码

protoc-IPATH=.proto文件所在得目录--java_out=java文件的输出路径.proto的名称

得到UserProtos类

3、编写序列化代码

1

2

3

4

5

6

7

8

9

10

11

12
Java代码

UserVoProtos.User.Builderbuilder=UserVoProtos.User.newBuilder();

builder.setName(
"Yaoming"
);builder.setAge(
30
);

builder.setPhone(13789878978L);

UserVoProtos.UserVo.Builderbuilder1=UserVoProtos.UserVo.newBuilder();

builder1.setName(
"tmac"
);builder1.setAge(
32
);builder1.setPhone(138999898989L);

UserVoProtos.UserVo.Builderbuilder2=UserVoProtos.UserVo.newBuilder();

builder2.setName(
"liuwei"
);builder2.setAge(
29
);builder2.setPhone(138999899989L);

builder.addFriends(builder1);

builder.addFriends(builder2);

UserVoProtos.UserVovo=builder.build();
byte
[]v=vo.toByteArray();

  

字节数53
反序列化

1

2

3
Java代码

UserVoProtos.UserVouvo=UserVoProtos.UserVo.parseFrom(dstb);

System.out.println(uvo.getFriends(
0
).getName());

 

结果:tmac,反序列化成功
googleprotobuf优点:字节数很小,适合网络传输节省io,跨语言。
缺点:需要依赖于工具生成代码。

工作机制
proto文件是对数据的一个描述,包括字段名称,类型,字节中的位置。protoc工具读取proto文件生成对应builder代码的类库。protocxxxxx--java_out=xxxxxx生成java类库。builder类根据自己的算法把数据序列化成字节流,或者把字节流根据反射的原理反序列化成对象。官方的示例:https://developers.google.com/protocol-buffers/docs/javatutorial。
proto文件中的字段类型和java中的对应关系:
详见:https://developers.google.com/protocol-buffers/docs/proto
.protoTypejavaTypec++Type
doubledoubledouble
floatfloatfloat
int32intint32
int64longint64
uint32intuint32
unint64longuint64
sint32intint32
sint64longint64
fixed32intuint32
fixed64longuint64
sfixed32intint32
sfixed64longint64
boolbooleanbool
stringStringstring
bytesbytestring
字段属性的描述:

写道

required:awell-formedmessagemusthaveexactlyoneofthisfield.optional:awell-formedmessagecanhavezerooroneofthisfield(butnotmorethanone).repeated:thisfieldcanberepeatedanynumberoftimes(includingzero)inawell-formedmessage.Theorderoftherepeatedvalueswillbepreserved.

protobuf在序列化和反序列化的时候,是依赖于.proto文件生成的builder类完成,字段的变化如果不表现在.proto文件中就不会影响反序列化,比较适合字段变化的情况。

做个测试:把UserVo序列化到文件中:

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
Java代码

UserProtos.Uservo=builder.build();

byte
[]v=vo.toByteArray();

FileOutputStreamfos=
new
FileOutputStream(dataFile);

fos.write(vo.toByteArray());

fos.close();



为User增加字段,对应的.proto文件:

Text代码

package
serialize;

optionjava_package=
"serialize"
;

optionjava_outer_classname=
"UserVoProtos"
;

messageUser{

optionalstringname=
1
;

optionalint32age=
2
;

optionalint64phone=
3
;

repeatedserialize.UserVofriends=
4
;

optionalstringaddress=
5
;}



从文件中反序列化回来:

Java代码

FileInputStreamfis=
new
FileInputStream(dataFile);

byte
[]dstb=
new
byte
[fis.available()];

for
(
int
i=
0
;i<dstb.length;i++){dstb[i]=(
byte
)fis.read();}

fis.close();UserProtos.Useruvo=UserProtos.User.parseFrom(dstb);

System.out.println(uvo.getFriends(
0
).getName());

成功得到结果。

三种方式对比传输同样的数据,googleprotobuf只有53个字节是最少的。结论:

方式优点缺点
JSON跨语言、格式清晰一目了然字节数比较大,需要第三方类库
ObjectSerializejava原生方法不依赖外部类库字节数比较大,不能跨语言
Googleprotobuf跨语言、字节数比较少编写.proto配置用protoc工具生成对应的代码
学习Java的同学注意了!!!
学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:589809992我们一起学Java!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: