【Java拾遺】不可不知的 Java 序列化
作者:Kiwifly
來源:SegmentFault 思否社區(qū)
【Java拾遺】不可不知的 Java 序列化

前言
在程序運(yùn)行的生命周期中,序列化與反序列化的操作,幾乎無時(shí)無刻不在發(fā)生著。對(duì)于任何一門語言來說,不管它是編譯型還是解釋型,只要它需要通訊或者持久化時(shí),就必然涉及到序列化與反序列化操作。但是,又正因?yàn)樾蛄谢c反序列化太過重要,太過普遍,大部分編程語言和框架都對(duì)其進(jìn)行了很好的封裝,又因?yàn)樗臐?rùn)物細(xì)無聲,使得我們很多時(shí)候根本沒有意識(shí)到,代碼下面其實(shí)進(jìn)行了許許多多序列化相關(guān)的操作。今天我們就一起去探尋這位最熟悉的陌生人。
序列化是什么
百度百科中給序列化的定義是『序列化 (Serialization)是將對(duì)象的狀態(tài)信息轉(zhuǎn)換為可以存儲(chǔ)或傳輸?shù)男问降倪^程。』。似乎有點(diǎn)抽象,下面用一個(gè)例子簡(jiǎn)單類比一下。
日常生活中,總少不了人跟人之間的交流與溝通。而溝通的前提是先要把我們大腦中想的內(nèi)容,通過某種形式表達(dá)出來。然后別人再通過我們表達(dá)出的內(nèi)容去理解。
而表達(dá)的方式多種多樣,最常見的就是說話,我們通過說一些話,把我們腦海里想的內(nèi)容表達(dá)出來,對(duì)方聽了這些話立刻明白了我們的想法。當(dāng)然表達(dá)也可以是文字,比如你正在看的本文,不也是在與你交流嗎?導(dǎo)演通過電影去表達(dá)自己對(duì)于世界的理解,畫家通過畫作述說的對(duì)美的渴望,音樂家通過樂符描述著對(duì)自由的向往。凡此種種,不勝枚舉。
所以,這些又跟我們的主題?序列化?有什么關(guān)系呢?
其實(shí)人與人之間少不了溝通交流,程序與程序之間,機(jī)器與機(jī)器之間也少不了溝通交流。只不過通常不會(huì)說是溝通,我們會(huì)說請(qǐng)求、響應(yīng)、傳輸、通訊…… 同樣的內(nèi)容只是換了一種說法。
上文中提到,人與人之間的溝通需要一種表達(dá)方式。通過這種表達(dá)方式把我們大腦中所想的內(nèi)容,轉(zhuǎn)化成他人可以理解的內(nèi)容。而機(jī)器與機(jī)器之間的通訊也需要這樣一種表達(dá)方式,通過這種表達(dá)方式把內(nèi)存中的內(nèi)容,轉(zhuǎn)化成其它機(jī)器可以讀取的內(nèi)容。
所以序列化可以簡(jiǎn)單的理解成是?機(jī)器內(nèi)存中信息的表達(dá)方式?。
為什么需要序列化
通常情況下,我們的語言一方面用于交流,比如聊天,把我腦海中的思想,通過語言表達(dá)出來,對(duì)方聽到我們的話語,會(huì)意我們的想法。
另一方面,我們的語言除了用于溝通交流,還可以用于記錄。有一句話叫做『好記性不如爛筆頭』。說的就是記錄的重要性,因?yàn)樵捲谖覀兊哪X子里,很容易就忘了,通過記錄下來可以保存更久。
而序列化功能又正好對(duì)應(yīng)這兩點(diǎn),一個(gè)是用來傳輸信息,另一個(gè)是用來持久化。序列化用來傳輸?shù)淖饔茫拔囊呀?jīng)說過了,關(guān)于持久化的作用,也很好理解。首先明確一個(gè)問題,序列化的是什么內(nèi)容?通常是內(nèi)存中的內(nèi)容。而內(nèi)存有一個(gè)特點(diǎn)我們都知道,那就是一重啟就沒了。對(duì)于部分內(nèi)容,我們想在重啟后還存在(比如說 tomcat 中 session 里面的對(duì)象),要怎么辦呢?答案就是把內(nèi)存中的對(duì)象保存到磁盤上,這樣就不怕重啟了,而持久化就需要用到序列化技術(shù)。
如何實(shí)現(xiàn)序列化
人與人之間有許許多多的表達(dá)方式,而且機(jī)器與機(jī)器之間也同樣,序列化的方式多種多樣。
Java 原生形式
對(duì)于如此普遍的序列化需求,Java 其實(shí)早在 JDK 1.1 開始就在語言層面進(jìn)行了支持。而且使用起來非常方便,下面我們就一起看看具體代碼。
首先我們要把想序列化的類實(shí)現(xiàn) Java 自帶的?java.io.Serializable?接口
/*
?*
?*??*?*
?*??*??*?blog.coder4j.cn
?*??*??*?Copyright?(C)?2016-2020?All?Rights?Reserved.
?*??*
?*
?*/
package?cn.coder4j.study.example.serialization;
import?java.io.Serializable;
import?java.util.StringJoiner;
/**
?*?@author?buhao
?*?@version?HaveSerialization.java,?v?0.1?2020-09-17?16:58?buhao
?*/
public?class?HaveSerialization?implements?Serializable?{
????private?static?final?long?serialVersionUID?=?-4504407589319471384L;
????private?String?name;
????private?Integer?age;
????/**
?????*?Getter?method?for?property?name.
?????*
?????*?@return?property?value?of?name
?????*/
????public?String?getName()?{
????????return?name;
????}
????/**
?????*?Setter?method?for?property?name.
?????*
?????*?@param?name?value?to?be?assigned?to?property?name
?????*/
????public?void?setName(String?name)?{
????????this.name?=?name;
????}
????/**
?????*?Getter?method?for?property?age.
?????*
?????*?@return?property?value?of?age
?????*/
????public?Integer?getAge()?{
????????return?age;
????}
????/**
?????*?Setter?method?for?property?age.
?????*
?????*?@param?age?value?to?be?assigned?to?property?age
?????*/
????public?void?setAge(Integer?age)?{
????????this.age?=?age;
????}
????@Override
????public?String?toString()?{
????????return?new?StringJoiner(",?",?HaveSerialization.class.getSimpleName()?+?"[",?"]")
????????????????.add("name='"?+?name?+?"'")
????????????????.add("age="?+?age)
????????????????.toString();
????}
}
需要注意的是,雖說是實(shí)現(xiàn)了 java.io.Serializable 接口,但是我們其實(shí)沒有覆蓋任何方法。這是為什么呢?我們一起看一下 java.io.Serializable 的源碼。
public?interface?Serializable?{
}
沒錯(cuò),是個(gè)空接口,除了接口定義部分,啥也沒有。通常遇到這種情況,我們稱之為標(biāo)記接口,主要為了標(biāo)記某些類,標(biāo)記的原因是,把它與其它類區(qū)別出來,方便我們后面專門處理。而 ?Serializable?這個(gè)標(biāo)記接口,就是為了讓我們知道這個(gè)類是要進(jìn)行序列化操作的類,僅此而已。
另外,雖然我們只實(shí)現(xiàn)一個(gè)空接口,但是細(xì)心的你,肯定發(fā)現(xiàn)了我們的類中多了一個(gè)?serialVersionUID?屬性。那么這個(gè)屬性的作用是什么呢?
它主要目的就是為了驗(yàn)證序列化與反序列化的類是否一致。比如上面 HaveSerialization 這個(gè)類現(xiàn)在有業(yè)務(wù)屬性 name 與 age ,現(xiàn)在因?yàn)闃I(yè)務(wù)需要,我們要添加一個(gè) address 的屬性。序列化操作是沒有問題的,但是把序列化信息傳輸給其它機(jī)器,其它機(jī)器在反序列化的時(shí)候,就出現(xiàn)了問題。因?yàn)槠渌鼨C(jī)器的 HaveSerialization 沒有 address 這個(gè)屬性。
為了解決這個(gè)問題,JDK 通過使用 serialVersionUID 在作為該類的版本號(hào),在反序列化時(shí)比較傳輸?shù)念惖闹蹬c要反序列化類的值是否一致,不一致就會(huì)報(bào) InvalidCastException 。
當(dāng)然,出發(fā)點(diǎn)是好的,但是直接拋異常會(huì)導(dǎo)致業(yè)務(wù)無法進(jìn)行下去,通常 serialVersionUID 生成好后,我們不會(huì)再更新,序列化如果沒有更新,對(duì)應(yīng)變更的屬性會(huì)為空,我們只要在業(yè)務(wù)里做好兼容就好了。
序列化對(duì)象
好了,我們已經(jīng)完成了第一步,定義了一個(gè)序列化類,下面我們就把他給序列化掉。
/**
?????*?序列化對(duì)象(保存序列化文件)
?????*?@throws?IOException
?????*/
????@Test
????public?void?testSaveSerializationObject()?throws?IOException?{
????????//?創(chuàng)建對(duì)象
????????final?HaveSerialization?haveSerialization?=?new?HaveSerialization();
????????haveSerialization.setName("kiwi");
????????haveSerialization.setAge(18);
????????//?創(chuàng)建序列化對(duì)象保存的文件
????????final?File?file?=?new?File("haveSerialization.ser");
????????//?創(chuàng)建對(duì)象輸出流
????????try?(final?ObjectOutputStream?objectOutputStream?=?new?ObjectOutputStream(new?FileOutputStream(file)))?{
????????????//?將對(duì)象輸出到序列化文件
????????????objectOutputStream.writeObject(haveSerialization);
????????}
????}
可以看到代碼十分簡(jiǎn)單,大體分成如下 4 步:
創(chuàng)建要序列化的對(duì)象
其實(shí)就是你上面的實(shí)現(xiàn)?java.io.Serializable?的類,如果沒有實(shí)現(xiàn),在這里會(huì)報(bào)?NotSerializableException?異常創(chuàng)建一個(gè) File 對(duì)象,用來保存序列化后的二進(jìn)制數(shù)據(jù)。
注意這里文件名我用的是?*.ser?,這個(gè)?ser?后綴并沒有強(qiáng)制要求,只是方便理解,你可能寫成其它后綴創(chuàng)建對(duì)象輸出流
創(chuàng)建一個(gè) ObjectOutputStream 對(duì)象輸出流的對(duì)象,并把上面定義的序列化文件對(duì)象通過構(gòu)造函數(shù)傳給它。通過輸出流把對(duì)象寫到序列化文件里
注意這里我用的?JDK 8?的?try with resource?語法,所以不用手動(dòng)?close
到這里我們序列化也完成了。
反序列化對(duì)象
既然有序列化,那肯定也有反序列化。反序列化可以理解成是序列化的逆向操作,既然序列化把內(nèi)存中的對(duì)象轉(zhuǎn)成一個(gè)可以持久化的文件,那么反序列化要做的就是把這個(gè)文件再加載到內(nèi)存中的對(duì)象。話不多說,直接看代碼。
/**
?????*?反序列化對(duì)象(從序列化文件中讀取對(duì)象)
?????*?@throws?IOException
?????*?@throws?ClassNotFoundException
?????*/
????@Test
????public?void?testLoadSerializationObject()?throws?IOException,?ClassNotFoundException?{
????????//?創(chuàng)建對(duì)象輸出流
????????try?(ObjectInputStream?objectInputStream?=?new?ObjectInputStream(
????????????????new?FileInputStream(new?File("haveSerialization.ser"))))?{
????????????//?從輸出流中創(chuàng)建對(duì)象
????????????final?Object?obj?=?objectInputStream.readObject();
????????????System.out.println(obj);
????????}
????}
反序列化代碼比序列化代碼還少,主要分成如下 2 步:
創(chuàng)建對(duì)象輸入流
創(chuàng)建一個(gè)?ObjectInputStream?對(duì)象,并把序列化文件通過構(gòu)造函數(shù)傳給它從對(duì)象輸入流中讀取對(duì)象
直接通過?readObject?方法即可,注意讀取后是?Object?類型,后續(xù)使用需手動(dòng)強(qiáng)轉(zhuǎn)一次
到這里,我們便通過 JDK 原生的方法完成了序列化與反序列化操作,是不是還很簡(jiǎn)單。但是日常工作不太推薦直接使用原生的方式實(shí)現(xiàn)序列化,一方面它生成的序列化文件較大,一方面也比一些第三方框架生成的慢,但是序列化原理大致類似。下面我們簡(jiǎn)單看一下其它方式如何序列化。
通用對(duì)象序列化
通常序列化是與語言綁定的,比如說通過上面 JDK 序列化的文件,不可能拿給 PHP 應(yīng)用反序列化成 PHP 的對(duì)象。不過可以通過某些特殊的通用對(duì)象結(jié)構(gòu)序列化來實(shí)現(xiàn)跨語言使用,比較常見的是 JSON 、XML 。下面我們以 JSON 為例看一下
/**
?????*?測(cè)試序列化通過json
?????*/
????@Test
????public?void?testSerializationByJSON(){
????????//-------------序列化操作---------------
????????//?創(chuàng)建對(duì)象
????????final?HaveSerialization?haveSerialization?=?new?HaveSerialization();
????????haveSerialization.setName("kiwi");
????????haveSerialization.setAge(18);
????????//?序列化成?JSON?字符串
????????final?String?jsonString?=?JSON.toJSONString(haveSerialization);
????????System.out.println("JSON:"?+?jsonString);
????????//-------------反序列化操作---------------
????????final?HaveSerialization?haveSerializationByJSON?=?JSON.parseObject(jsonString,?HaveSerialization.class);
????????System.out.println(haveSerializationByJSON);
????}
運(yùn)行結(jié)果:
JSON:{"age":18,"name":"kiwi"}
HaveSerialization[name='kiwi',?age=18]
上述代碼使用的 JSON 框架是 alibaba/fastjson 。但是大部分 JSON 框架使用起來都大同小異。可以按個(gè)人喜好去替換。
序列化框架
序列化框架其實(shí)有很多,比如 kryo 、 hessian 、 protostuff 。它們各有優(yōu)缺點(diǎn),詳細(xì)的比較可以看這篇文章?序列化框架 kryo VS hessian VS Protostuff VS java 。大家可以按各自的使用場(chǎng)景選擇使用,下文以 kryo 為例演示。
依賴
????com.esotericsoftware
????kryo
????5.0.0-RC9
具體代碼
/**
?????*?測(cè)試序列化通過kryo
?????*/
????@Test
????public?void?testSerializationByKryo()?throws?FileNotFoundException?{
????????//-------------序列化操作---------------
????????//?創(chuàng)建對(duì)象
????????final?HaveSerialization?haveSerialization?=?new?HaveSerialization();
????????haveSerialization.setName("kiwi");
????????haveSerialization.setAge(18);
????????final?Kryo?kryo?=?new?Kryo();
????????//?注冊(cè)序列化類
????????kryo.register(HaveSerialization.class);
????????//?序列化操作
????????try?(final?Output?output?=?new?Output(new?FileOutputStream("haveSerialization.kryo")))?{
????????????kryo.writeObject(output,?haveSerialization);
????????}
????????//?反序列化
????????try?(final?Input?input?=?new?Input(new?FileInputStream("haveSerialization.kryo")))?{
????????????final?HaveSerialization?haveSerializationByKryo?=?kryo.readObject(input,?HaveSerialization.class);
????????????System.out.println(haveSerializationByKryo);
????????}
????}
其實(shí)看代碼可以發(fā)現(xiàn)跟 JDK 的流程幾乎一樣,其中有幾點(diǎn)需要注意的,kryo 在序列化前,要手動(dòng)通過 register 注冊(cè)序列化的類,有點(diǎn)類似 JDK 實(shí)現(xiàn) java.io.Serializable 接口。然后 Input 、 Output 對(duì)象不是 JDK 的。是 kryo 提供的。另外 Kryo 有不少需要注意的地方,可以查看參考鏈接部分的內(nèi)容學(xué)習(xí)。
源碼地址
因文章篇幅有限,無法展示所有代碼,已經(jīng)另外把完整代碼上傳到 github,具體鏈接如下:
https://github.com/kiwiflydream/study-example/tree/master/study-serialization-example
總結(jié)
本文主要介紹了 Java 序列化的相關(guān)內(nèi)容,主要介紹序列化是什么?與人與人之間溝通的表達(dá)方式做類比,得到是?機(jī)器內(nèi)存中信息的表達(dá)方式?。而為什么需要序列化,我們通過舉例說明了序列化?信息傳輸與持久化?的功能。最后我們一起從 JDK 原生的實(shí)現(xiàn) java.io.Serializable 的方式,再到通用對(duì)象序列化的 JSON、XML 方式,最終到第三方框架 kryo 的形式了解如何去實(shí)現(xiàn)序列化。

