公眾號(hào):追風(fēng)棧Binary
我們通過(guò)文本或者數(shù)據(jù)庫(kù)的方式來(lái)永久化存儲(chǔ)程序中產(chǎn)生的數(shù)據(jù)凭迹。這種思路也可以借鑒到保存Java對(duì)象這類問(wèn)題上來(lái),因?yàn)镴ava對(duì)象的存在是臨時(shí)性的官疲,等到程序結(jié)束后就會(huì)被垃圾回收機(jī)制進(jìn)行回收。所以需要特定的手段來(lái)完成相應(yīng)的轉(zhuǎn)換過(guò)程。
什么是Java對(duì)象的序列化和反序列化摧冀?
以一個(gè)POJO(Plain Ordinary Java Object,簡(jiǎn)單的Java對(duì)象)
的User
類為例,當(dāng)我們創(chuàng)建了一個(gè)User
對(duì)象myUser
之后系宫,這個(gè)myUser
對(duì)象就會(huì)存儲(chǔ)在JVM
(Java虛擬機(jī))上的堆內(nèi)存中索昂,只要垃圾回收機(jī)制沒(méi)有盯上myUser
,那么在程序運(yùn)行期間我們總能有方法來(lái)訪問(wèn)到這個(gè)對(duì)象所包含的屬性以及方法扩借。而當(dāng)JVM
停止運(yùn)行椒惨,那么myUser
就會(huì)被回收,我們就再也拿不到myUser
所包含的屬性和方法了往枷。
那問(wèn)題是,如果我們需要保存這個(gè)myUser
對(duì)象的狀態(tài)信息凄杯,并加以保存错洁,并在以后有需要的情況下將它原封不動(dòng)的加載回來(lái)。那這個(gè)過(guò)程戒突,就是Java對(duì)象的序列化與反序列化屯碴。我自己將這個(gè)過(guò)程畫(huà)了個(gè)圖,如下:
從術(shù)語(yǔ)角度來(lái)說(shuō)膊存,
Serialization
是Java內(nèi)置的一個(gè)API
导而,通過(guò)這個(gè)步驟,將對(duì)象狀態(tài)信息
轉(zhuǎn)化為字節(jié)流的方式隔崎,用于存儲(chǔ)或者作為信息進(jìn)行傳輸今艺;而后再根據(jù)需要,通過(guò)反序列化的方式爵卒,將字節(jié)流轉(zhuǎn)換成初始的對(duì)象虚缎。例如上圖中類生成的對(duì)象小明,我們可以把他變成一種文本方式存儲(chǔ)起來(lái)钓株,等需要小明工作的時(shí)候实牡,再將小明復(fù)原陌僵。這就是序列化和反序列的基本原理。
什么是Serializable创坞?
Serializable
是Java io
包中的一個(gè)內(nèi)置接口碗短,這個(gè)接口非常的有意思,從上面的過(guò)程來(lái)看题涨,要實(shí)現(xiàn)這個(gè)接口的功能應(yīng)該很復(fù)雜才對(duì)偎谁,但實(shí)際上,我們通過(guò)IDEA打開(kāi)它的源碼携栋,就會(huì)發(fā)現(xiàn):
里面啥也沒(méi)有搭盾,只是一個(gè)
Serializable
接口聲明。實(shí)際上婉支,這個(gè)接口它僅用作標(biāo)識(shí)實(shí)現(xiàn)該接口的類可以被序列化鸯隅。在序列化對(duì)象的過(guò)程中,聲明實(shí)現(xiàn)該接口是必須的向挖,如果上圖中的類不實(shí)現(xiàn)Serializable
這個(gè)接口蝌以,就會(huì)報(bào)出NotSerializationException
這個(gè)異常。
在使用前應(yīng)該注意的幾個(gè)問(wèn)題
在實(shí)現(xiàn)序列化和反序列化的過(guò)程中何之,需要注意幾個(gè)問(wèn)題跟畅。
靜態(tài)變量在序列化過(guò)程中并不會(huì)被保存:序列化保存的是對(duì)象狀態(tài),而靜態(tài)變量是屬于類變量溶推,因此并不會(huì)被保存徊件。
一個(gè)類能不能序列化,就看它是否實(shí)現(xiàn)
Serializable
這個(gè)接口蒜危。并且如果子類的父類實(shí)現(xiàn)了該接口虱痕,那么這個(gè)子類也可以實(shí)現(xiàn)序列化。serialVersionUID
:這是一個(gè)與可序列化的類相關(guān)聯(lián)的版本號(hào)辐赞,在反序列化的過(guò)程中部翘,這個(gè)序列號(hào)用來(lái)驗(yàn)證是不是該類進(jìn)行的序列化過(guò)程。匹配成功响委,則進(jìn)行反序列化新思,若serialVersionUID
匹配不成功,那么就會(huì)拋出InvalidClassException
這個(gè)異常赘风。雖然可以不人為設(shè)定這個(gè)數(shù)值夹囚,但該接口的API
強(qiáng)烈建議開(kāi)發(fā)者自行對(duì)這個(gè)版本號(hào)進(jìn)行顯式聲明,并且規(guī)定這個(gè)值必須是static
類型和final
類型邀窃。通常一般我們也會(huì)將其設(shè)為priavte
私有變量的表述方式崔兴,例如:priavte static final long serialVersionUID = 1L
-
Transient
關(guān)鍵字控制的變量不會(huì)序列化到目標(biāo)文件中,當(dāng)反序列化完成后,這個(gè)Transient
修飾的變量將被賦予Java規(guī)定的初始值敲茄。例如在下圖中位谋,字符串gender
被賦予了默認(rèn)值null
如何持久化一個(gè)對(duì)象:序列化與反序列化?
以第一段中的User
類為例堰燎,對(duì)這個(gè)User
類的對(duì)象進(jìn)行序列化過(guò)程掏父。User
類是一個(gè)簡(jiǎn)單的JaveBean
,它實(shí)現(xiàn)了序列化的接口Serializable
秆剪,意味著這個(gè)類可以序列化赊淑。其中gender
屬性被transient
修飾,意味著這個(gè)變量將不會(huì)被序列化仅讽。除此之外陶缺,serialVersionUID
被設(shè)置為1L
,注意long
長(zhǎng)整型的后面需要加L
修飾洁灵,并用大寫(xiě)字母L
修飾饱岸,不要用小寫(xiě)l
,避免與數(shù)字1發(fā)生混淆徽千。
public class User implements Serializable {
?
private String name;
private int age;
private String work;
private transient String gender;
private String words;
private static final long serialVersionUID = 1L;
?
public String getName() {
return name;
}
?
public void setName(String name) {
this.name = name;
}
?
public int getAge() {
return age;
}
?
public void setAge(int age) {
this.age = age;
}
?
public String getWork() {
return work;
}
?
public void setWork(String work) {
this.work = work;
}
?
public String getGender() {
return gender;
}
?
public void setGender(String gender) {
this.gender = gender;
}
?
public String getWords() {
return words;
}
?
public void setWords(String words) {
this.words = words;
}
?
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", work='" + work + '\'' +
", gender='" + gender + '\'' +
", words='" + words + '\'' +
'}';
}
}
在定義了User
類后苫费,首先是利用該類創(chuàng)建一個(gè)對(duì)象小明。
User user = new User();
user.setName("小明");
user.setGender("male");
user.setAge(25);
user.setWork("Programmer");
user.setWords("hello");</pre>
然后是對(duì)象的序列化過(guò)程双抽,在這個(gè)步驟中需要ObjectOutputStream
將對(duì)象的基本數(shù)據(jù)類型寫(xiě)入流中百框。ObjectOutputStream
很獨(dú)特,它只能將支持Serializable
接口的對(duì)象寫(xiě)入字節(jié)流牍汹。默認(rèn)的對(duì)象序列化機(jī)制寫(xiě)入字節(jié)流的內(nèi)容包括:對(duì)象的類铐维、類簽名、非靜態(tài)字段的值∩鞣疲現(xiàn)在我們需要把小明這個(gè)對(duì)象的狀態(tài)寫(xiě)入文本中加以保存嫁蛇。
//將對(duì)象寫(xiě)入字節(jié)流
ObjectOutputStream objectOutputStream;
try{
// 將一個(gè)OutputStream對(duì)象作為構(gòu)造函數(shù)的參數(shù)創(chuàng)建對(duì)象,這里是文件對(duì)象
objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));
// 將小明這個(gè)對(duì)象寫(xiě)入到這個(gè)文本中
objectOutputStream.writeObject(user);
//關(guān)閉流
objectOutputStream.close();
}catch (IOException e){
e.printStackTrace();
}
運(yùn)行程序钧嘶,就可以看到文件夾下生成了名為tempFile
的文件棠众,這就完成了對(duì)小明這個(gè)對(duì)象序列化的過(guò)程琳疏。打開(kāi)文件查看里面的內(nèi)容有决,但是會(huì)發(fā)現(xiàn)我們并不明白這到底寫(xiě)的是什么,但是這個(gè)就是小明被序列化后的模樣空盼。
由于寫(xiě)入的是字節(jié)流书幕,因此對(duì)閱讀者并不友好。后續(xù)會(huì)用一種更為完善的方法來(lái)解決這種問(wèn)題揽趾。
在序列化之后台汇,我們又突然需要復(fù)原小明,這就必須進(jìn)行對(duì)象的反序列化過(guò)程。反序列化過(guò)程與序列化過(guò)程類似苟呐,它需要ObjectInputStream
方法來(lái)讀入需要復(fù)原的字節(jié)流痒芝,然后調(diào)用readObject
的方法轉(zhuǎn)換成對(duì)象。在這個(gè)過(guò)程中牵素,會(huì)對(duì)序列號(hào)serialVersionUID
進(jìn)行校驗(yàn)工作严衬,以確保類型匹配正確。
File file = new File("tempFile");
ObjectInputStream objectInputStream;
try{
// 將存儲(chǔ)小明對(duì)象的文件流作為參數(shù)來(lái)創(chuàng)建一個(gè)對(duì)象
objectInputStream = new ObjectInputStream(new FileInputStream(file));
// 從這個(gè)字節(jié)流中讀取對(duì)象笆呆,并強(qiáng)制轉(zhuǎn)換為User類對(duì)象
User myUser = (User) objectInputStream.readObject();
System.out.println(myUser);
}catch (IOException e){
e.printStackTrace();
}catch (ClassNotFoundException e){
e.printStackTrace();
}
從控制臺(tái)中查看復(fù)原的小明请琳,看看他和我們序列化之前有什么區(qū)別:
可以看出,小明的gender
因?yàn)楸?code>transient修飾赠幕,并沒(méi)有被序列化俄精,因此反序列化后字段被賦予了默認(rèn)值null
,就這樣榕堰,小明被反序列化為一個(gè)身份不明的人竖慧。
序列化和反序列化在實(shí)際應(yīng)用中十分廣泛,例如遠(yuǎn)程通信傳輸對(duì)象信息的時(shí)候局冰,就需要將對(duì)象轉(zhuǎn)換成網(wǎng)絡(luò)所允許的二進(jìn)制字節(jié)流测蘑。但是在上面我們也發(fā)現(xiàn),保存到本地的對(duì)象文本由于編碼問(wèn)題康二,導(dǎo)致可讀性很差碳胳,有沒(méi)有什么更好的替代方案呢?
Json與FastJson庫(kù)
Json是一種輕量級(jí)的數(shù)據(jù)交換格式沫勿,在實(shí)際的工作中挨约,Json的使用非常的頻繁。Json通過(guò)鍵值對(duì)來(lái)表示對(duì)象的屬性和屬性值产雹。如下的Demo.json
展示了Json文件的一般形式诫惭。
Json文件的特點(diǎn)在于結(jié)構(gòu)清晰,簡(jiǎn)明易讀蔓挖。并且在序列化與反序列化上夕土,Json使用起來(lái)也更方便。在這個(gè)問(wèn)題上瘟判,阿里巴巴提供了一個(gè)非常好用的工具FastJson怨绣,它是阿里巴巴開(kāi)源的Json解析庫(kù),并且支持將JavaBean序列化為Json字符串拷获,也可以從Json字符串反序列為JavaBean篮撑。
FastJson使用非常簡(jiǎn)單,如果不是使用Maven構(gòu)建項(xiàng)目匆瓜,那就需要下載FastJson的Jar包進(jìn)行導(dǎo)入赢笨,在IDEA中按照如下步驟添加FastJson庫(kù)
點(diǎn)擊IDEA的
File
未蝌,彈出后選擇Project Structure
在彈出的頁(yè)面選擇
Modules
,并在隨后的頁(yè)面中選擇Dependencies
在
Dependencies
頁(yè)面下點(diǎn)擊右側(cè)綠色的+
號(hào)茧妒,彈出的小菜單中選擇JARs or directories
導(dǎo)入自己路徑下的FastJson的Jar包萧吠,點(diǎn)擊
Apply
后點(diǎn)擊ok
測(cè)試FastJson庫(kù)是否正常運(yùn)行
使用FastJson對(duì)JavaBean進(jìn)行序列化與反序列化簡(jiǎn)直不要太簡(jiǎn)單了,兩個(gè)過(guò)程都只需要一行代碼便可以完成相關(guān)操作桐筏。例如FastJson對(duì)小明進(jìn)行序列化怎憋,見(jiàn)如下代碼:
User user = new User();
user.setName("小明");
user.setGender("male");
user.setAge(25);
user.setWork("Programmer");
user.setWords("hello");
?
String jsonData = JSONObject.toJSONString(user);
System.out.println(jsonData);
通過(guò)JSONObject調(diào)用toJSONString
的方法,傳入需要序列化的對(duì)象九昧,只需要一行代碼绊袋,就可以得到對(duì)象序列化之后的字符串。通過(guò)打印就可以看出這個(gè)對(duì)象里面到底有些啥铸鹰。
依舊可以看出癌别,被transient
修飾的gender
并沒(méi)有被序列化,而這一次蹋笼,我們很直觀的看到了小明這個(gè)對(duì)象對(duì)序列化之后的結(jié)果展姐。
那么反序列化過(guò)程也同樣簡(jiǎn)潔:
User myUser = JSONObject.parseObject(jsonData, User.class);
System.out.println(myUser);
反序列化過(guò)程的parseObject
方法接受一個(gè)序列化后的字符串和目標(biāo)對(duì)象的類,生成一個(gè)反序列化的對(duì)象后剖毯,通過(guò)User
聲明的myUser
引用反序列化后的對(duì)象圾笨,打印這個(gè)對(duì)象,得到如下的結(jié)果:
注意這個(gè)對(duì)象的輸出結(jié)果與上面序列化后的結(jié)果的差異逊谋,gender
反序列化后被賦予了null
擂达,其實(shí)這也說(shuō)明了反序列化過(guò)程按照類的模板進(jìn)行完全匹配的,對(duì)于沒(méi)能序列化的對(duì)象狀態(tài)胶滋,反序列化都盡量來(lái)恢復(fù)對(duì)象的初始狀態(tài)板鬓。
由此可以看出,Json文件和FastJson庫(kù)在Java的序列化和反序列化的作用效率會(huì)更高究恤,并且JavaBean不需要實(shí)現(xiàn)序列化Serializable
接口俭令,直接就配合FastJson可以使用,大幅提升了編碼質(zhì)量部宿。
總結(jié)
在實(shí)際工作場(chǎng)景中抄腔,序列化和反序列化是一個(gè)十分常見(jiàn)常用的功能,現(xiàn)如今大部分都在采納Json和FastJson解析庫(kù)結(jié)合的這種方式進(jìn)行交互理张,但是對(duì)于Java中Serializable的基本性質(zhì)還是需要了解赫蛇。更深入的,對(duì)于Serializable中的writeObject
方法和readObject
方法實(shí)現(xiàn)自定義的序列化策略也還需要進(jìn)一步的研究和實(shí)踐涯穷,后續(xù)會(huì)及時(shí)更新棍掐。