文着重講解一下Java序列化的相關內容。
如果對Java序列化感興趣的同學可以研究一下。
一.Java序列化的作用
有的時候我們想要把一個Java對象變成字節流的形式傳出去,有的時候我們想要從一個字節流中恢復一個Java對象。例如,有的時候我們想要
把一個Java對象寫入到硬盤或者傳輸到網路上面的其它計算機,這時我們就需要自己去通過java把相應的對象寫成轉換成字節流。對於這種通用
的操作,我們為什么不使用統一的格式呢?沒錯,這里就出現了java的序列化的概念。在Java的OutputStream類下面的子類ObjectOutput-
Stream類就有對應的WriteObject(Object object) 其中要求對應的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
Integer id;
private
String title;
//文章標題
private
String content;
// 文章內容
private
String faceIcon;
//表情圖標
private
Date postTime;
//文章發表的時間
private
String ipAddr;
//用戶的ip
private
User author;
//回復的用戶
public
Integer getId() {
return
id;
}
public
void
setId(Integer id) {
this
.id = id;
}
public
String getTitle() {
return
title;
}
public
void
setTitle(String title) {
this
.title = title;
}
public
String getContent() {
return
content;
}
public
void
setContent(String content) {
this
.content = content;
}
public
String getFaceIcon() {
return
faceIcon;
}
public
void
setFaceIcon(String faceIcon) {
this
.faceIcon = faceIcon;
}
public
Date getPostTime() {
return
postTime;
}
public
void
setPostTime(Date postTime) {
this
.postTime = postTime;
}
public
User getAuthor() {
return
author;
}
public
void
setAuthor(User author) {
this
.author = author;
}
public
String getIpAddr() {
return
ipAddr;
}
public
void
setIpAddr(String ipAddr) {
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 {
/**
* The object
implements
the writeExternal method to save its contents
* by calling the methods of DataOutput
for
its primitive values or
|
沒錯,Externlizable接口繼承了java的序列化接口,並增加了兩個方法:
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, 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> String userName;
String password;
String age;
public
Person(String userName, String password, String age) {
super
();
this
.userName = userName;
this
.password = password;
this
.age = age;
}
public
Person() {
super
();
}
public
String getAge() {
return
age;
}
public
void
setAge(String age) {
this
.age = age;
}
public
String getUserName() {
return
userName;
}
public
void
setUserName(String userName) {
this
.userName = userName;
}
public
String getPassword() {
return
password;
}
public
void
setPassword(String password) {
this
.password = password;
}
/**
* 序列化操作的擴展類
*/
@Override
public
void
writeExternal(ObjectOutput out)
throws
IOException {
//增加一個新的對象
Date date=
new
Date();
out.writeObject(userName);
out.writeObject(password);
out.writeObject(date);
}
/**
* 反序列化的擴展類
*/
@Override
public
void
readExternal(ObjectInput in)
throws
IOException,
ClassNotFoundException {
//注意這里的接受順序是有限制的哦,否則的話會出錯的
// 例如上面先write的是A對象的話,那么下面先接受的也一定是A對象...
userName=(String) in.readObject();
password=(String) in.readObject();
SimpleDateFormat sdf=
new
SimpleDateFormat(
"yyyy-MM-dd"
);
Date date=(Date)in.readObject();
System.out.println(
"反序列化后的日期為:"
+sdf.format(date));
}
@Override
public
String toString() {
//注意這里的年齡是不會被序列化的,所以在反序列化的時候是讀取不到數據的
return
"用戶名:"
+userName+
"密 碼:"
+password+
"年齡:"
+age;
}
}
/**
* 序列化和反序列化的相關操作類
* @author 小浩
* @創建日期 2015-3-12
*/
class
Operate{
/**
* 序列化方法
* @throws IOException
* @throws FileNotFoundException
*/
public
void
serializable(Person person)
throws
FileNotFoundException, IOException{
ObjectOutputStream outputStream=
new
ObjectOutputStream(
new
FileOutputStream(
"a.txt"
));
outputStream.writeObject(person);
}
/**
* 反序列化的方法
* @throws IOException
* @throws FileNotFoundException
* @throws ClassNotFoundException
*/
public
Person deSerializable()
throws
FileNotFoundException, IOException, ClassNotFoundException{
ObjectInputStream ois=
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 {
Operate operate=
new
Operate();
Person person=
new
Person(
"小浩"
,
"123456"
,
"20"
);
System.out.println(
"為序列化之前的相關數據如下:\n"
+person.toString());
operate.serializable(person);
Person newPerson=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
String name;
private
transient
String password;
private
static
int
count =
0
;
public
Student1(String name, String password) {
System.out.println(
"調用Student的帶參的構造方法"
);
this
.name = name;
this
.password = password;
count++;
}
public
String toString() {
return
"人數: "
+ count +
" 姓名: "
+ name +
" 密碼: "
+ password;
}
}
public
class
ObjectSerTest1 {
public
static
void
main(String args[]) {
try
{
FileOutputStream fos =
new
FileOutputStream(
"test.obj"
);
ObjectOutputStream oos =
new
ObjectOutputStream(fos);
Student1 s1 =
new
Student1(
"張三"
,
"12345"
);
Student1 s2 =
new
Student1(
"王五"
,
"54321"
);
oos.writeObject(s1);
oos.writeObject(s2);
oos.close();
FileInputStream fis =
new
FileInputStream(
"test.obj"
);
ObjectInputStream ois =
new
ObjectInputStream(fis);
Student1 s3 = (Student1) ois.readObject();
Student1 s4 = (Student1) ois.readObject();
System.out.println(s3);
System.out.println(s4);
ois.close();
}
catch
(IOException e) {
e.printStackTrace();
}
catch
(ClassNotFoundException e1) {
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(String args[]){
try
{
FileInputStream fis =
new
FileInputStream(
"test.obj"
);
ObjectInputStream ois =
new
ObjectInputStream(fis);
Student1 s3 = (Student1) ois.readObject();
Student1 s4 = (Student1) ois.readObject();
System.out.println(s3);
System.out.println(s4);
ois.close();
}
catch
(IOException e) {
e.printStackTrace();
}
catch
(ClassNotFoundException e1) {
e1.printStackTrace();
}
}
}
|
2.也是最應該注意的,如果你先序列化對象A后序列化B,那么在反序列化的時候一定記着JAVA規定先讀到的對象
是先被序列化的對象,不要先接收對象B,那樣會報錯.尤其在使用上面的Externalizable的時候一定要注意讀取
的先后順序。
3.實現序列化接口的對象並不強制聲明唯一的serialVersionUID,是否聲明serialVersionUID對於對象序列化的向
上向下的兼容性有很大的影響。我們來做個測試:
思路一
把User中的serialVersionUID去掉,序列化保存。反序列化的時候,增加或減少個字段,看是否成功。
保存到文件中:
增加或者減少字段后,從文件中讀出來,反序列化:
結果:拋出異常信息
Java代碼
思路二
eclipse指定生成一個serialVersionUID,序列化保存,修改字段后反序列化
略去代碼
結果:反序列化成功
結論
如果沒有明確指定serialVersionUID,序列化的時候會根據字段和特定的算法生成一個serialVersionUID,當屬性有變化時這個id發生了變化,所以反序列化的時候
就會失敗。拋出“本地classd的唯一id和流中class的唯一id不匹配”。
jdk文檔關於serialVersionUID的描述:
寫道
三.實現序列化的其它方式 (這是一個擴展內容,感興趣的可以擴展一下)
1)是把對象包裝成JSON字符串傳輸。
這里采用JSON格式同時使用采用Google的gson-2.2.2.jar 進行轉義
2)采用谷歌的ProtoBuf
隨着Google工具protoBuf的開源,protobuf也是個不錯的選擇。對JSON,Object Serialize(Java的序列化和反序列化),
ProtoBuf 做個對比。
定義一個通用的待傳輸的對象UserVo:
初始化User的實例src:
得到的字符串:
字節數為153
Json的優點:明文結構一目了然,可以跨語言,屬性的增加減少對解析端影響較小。缺點:字節數過多,依賴於不同的第三方類庫。
Object Serialize(Java的序列化和反序列化)
UserVo實現Serializalbe接口,提供唯一的版本號:
序列化方法:
字節數是238
反序列化:
Object Serializalbe 優點:java原生支持,不需要提供第三方的類庫,使用比較簡單。
缺點:無法跨語言,字節數占用比較大,某些情況下對於對象屬性的變化比較敏感。
對象在進行序列化和反序列化的時候,必須實現Serializable接口,但並不強制聲明唯一的serialVersionUID
是否聲明serialVersionUID對於對象序列化的向上向下的兼容性有很大的影響。
Google ProtoBuf
protocol buffers 是google內部得一種傳輸協議,目前項目已經開源(http://code.google.com/p/protobuf/)。
它定義了一種緊湊得可擴展得二進制協議格式,適合網絡傳輸,並且針對多個語言有不同得版本可供選擇。
以protobuf-2.5.0rc1為例,准備工作:
下載源碼,解壓,編譯,安裝
測試:
安裝成功!
進入源碼得java目錄,用mvn工具編譯生成所需得jar包,protobuf-java-2.5.0rc1.jar
1、編寫.proto文件,命名UserVo.proto
2、在命令行利用protoc 工具生成builder類
得到UserProtos類
3、編寫序列化代碼
字節數53
反序列化
結果:tmac,反序列化成功google protobuf 優點:字節數很小,適合網絡傳輸節省io,跨語言 。
缺點:需要依賴於工具生成代碼。
工作機制
proto文件是對數據的一個描述,包括字段名稱,類型,字節中的位置。protoc工具讀取proto文件生成對應builder代碼的類庫。protoc xxxxx --java_out=xxxxxx 生成java類庫。builder類根據自己的算法把數據序列化成字節流,或者把字節流根據反射的原理反序列化成對象。官方的示例:https://developers.google.com/protocol-buffers/docs/javatutorial。
proto文件中的字段類型和java中的對應關系:
詳見:https://developers.google.com/protocol-buffers/docs/proto
.proto Type | java Type | c++ Type |
double | double | double |
float | float | float |
int32 | int | int32 |
int64 | long | int64 |
uint32 | int | uint32 |
unint64 | long | uint64 |
sint32 | int | int32 |
sint64 | long | int64 |
fixed32 | int | uint32 |
fixed64 | long | uint64 |
sfixed32 | int | int32 |
sfixed64 | long | int64 |
bool | boolean | bool |
string | String | string |
bytes | byte | string |
方式 | 優點 | 缺點 |
JSON | 跨語言、格式清晰一目了然 |
字節數比較大,需要第三方類庫 |
Object Serialize | java原生方法不依賴外部類庫 | 字節數比較大,不能跨語言 |
Google protobuf | 跨語言、字節數比較少 |
編寫.proto配置用protoc工具生成對應的代碼 |