序列化與反序列化

引言

將 Java 對(duì)象序列化為二進(jìn)制文件的 Java 序列化技術(shù)是 Java 系列技術(shù)中一個(gè)較為重要的技術(shù)點(diǎn)院尔,在大部分情況下箫荡,開發(fā)人員只需要了解被序列化的類需要實(shí)現(xiàn) Serializable接口魁亦,
使用 ObjectInputStreamObjectOutputStream 進(jìn)行對(duì)象的讀寫。然而在有些情況下羔挡,光知道這些還遠(yuǎn)遠(yuǎn)不夠洁奈,文章列舉了筆者遇到的一些真實(shí)情境绞灼,它們與 Java 序列化相關(guān),
通過分析情境出現(xiàn)的原因低矮,使讀者輕松牢記 Java 序列化中的一些高級(jí)認(rèn)識(shí)。

文章結(jié)構(gòu)

本文將逐一的介紹幾個(gè)情境军掂,順序如下面的列表轮蜕。

序列化 ID 的問題
靜態(tài)變量序列化
父類的序列化與 Transient 關(guān)鍵字
對(duì)敏感字段加密
序列化存儲(chǔ)規(guī)則

列表的每一部分講述了一個(gè)單獨(dú)的情境蝗锥,讀者可以分別查看跃洛。

序列化 ID 問題

解決:虛擬機(jī)是否允許反序列化玛追,不僅取決于類路徑和功能代碼是否一致,一個(gè)非常重要的一點(diǎn)是兩個(gè)類的序列化 ID 是否一致
(就是 private static final long serialVersionUID = 1L)闲延。即使兩個(gè)類的功能代碼完全一致痊剖,但是序列化 ID 不同垒玲,他們也無法相互序列化和反序列化。

簡(jiǎn)單來說合愈,Java的序列化機(jī)制是通過在運(yùn)行時(shí)判斷類的serialVersionUID來驗(yàn)證版本一致性的。
在進(jìn)行反序列化時(shí)益老,JVM會(huì)把傳來的字節(jié)流中的serialVersionUID與本地相應(yīng)實(shí)體(類)的serialVersionUID進(jìn)行比較寸莫,
如果相同就認(rèn)為是一致的,可以進(jìn)行反序列化膘茎,否則就會(huì)出現(xiàn)序列化版本不一致的異常酷誓。

當(dāng)實(shí)現(xiàn)java.io.Serializable接口的實(shí)體(類)沒有顯式地定義一個(gè)名為serialVersionUID态坦,類型為long的變量時(shí),
Java序列化機(jī)制會(huì)根據(jù)編譯的class自動(dòng)生成一個(gè)serialVersionUID作序列化版本比較用玫氢,這種情況下壮锻,只有同一次編譯生成的class才會(huì)生成相同的serialVersionUID 。
如果我們不希望通過編譯來強(qiáng)制劃分軟件版本猜绣,即實(shí)現(xiàn)序列化接口的實(shí)體能夠兼容先前版本掰邢,未作更改的類,就需要顯式地定義一個(gè)名為serialVersionUID辣之,
類型為long的變量掰伸,不修改這個(gè)變量值的序列化實(shí)體都可以相互進(jìn)行串行化和反串行化。

序列化 ID 在 Eclipse 下提供了兩種生成策略怀估,一個(gè)是固定的 1L,一個(gè)是隨機(jī)生成一個(gè)不重復(fù)的 long 類型數(shù)據(jù)(實(shí)際上是使用 JDK 工具生成)多搀,在這里有一個(gè)建議,
如果沒有特殊需求惯退,就是用默認(rèn)的 1L 就可以从藤,這樣可以確保代碼一致時(shí)反序列化成功。那么隨機(jī)生成的序列化 ID 有什么作用呢懊蒸,有些時(shí)候悯搔,通過改變序列化 ID 可以用來限制某些用戶的使用。

靜態(tài)變量序列化

情境:查看清單 1 的代碼。
清單 1. 靜態(tài)變量序列化問題代碼

 public class Test implements Serializable {

    private static final long serialVersionUID = 1L;

    public static int staticVar = 5;

    public static void main(String[] args) {
        try {
            //初始時(shí)staticVar為5
            ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream("result.obj"));
            out.writeObject(new Test());
            out.close();

            //序列化后修改為10
            Test.staticVar = 10;

            ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
                    "result.obj"));
            Test t = (Test) oin.readObject();
            oin.close();
            
            //再讀取抡笼,通過t.staticVar打印新的值
            System.out.println(t.staticVar);
            
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

清單 1 中的 main 方法推姻,將對(duì)象序列化后框沟,修改靜態(tài)變量的數(shù)值藏古,再將序列化對(duì)象讀取出來忍燥,然后通過讀取出來的對(duì)象獲得靜態(tài)變量的數(shù)值并打印出來。依照清單 2梅垄,
這個(gè) System.out.println(t.staticVar) 語句輸出的是 10 還是 5 呢?

最后的輸出是 10靡馁,對(duì)于無法理解的讀者認(rèn)為机久,打印的 staticVar 是從讀取的對(duì)象里獲得的,應(yīng)該是保存時(shí)的狀態(tài)才對(duì)胧弛。之所以打印 10 的原因在于序列化時(shí)侠畔,
并不保存靜態(tài)變量,這其實(shí)比較容易理解践图,序列化保存的是對(duì)象的狀態(tài)码党,靜態(tài)變量屬于類的狀態(tài)斥黑,因此 序列化并不保存靜態(tài)變量。

父類的序列化與 Transient 關(guān)鍵字

情境:一個(gè)子類實(shí)現(xiàn)了 Serializable 接口锌奴,它的父類都沒有實(shí)現(xiàn) Serializable 接口,序列化該子類對(duì)象箕慧,然后反序列化后輸出父類定義的某變量的數(shù)值,該變量數(shù)值與序列化時(shí)的數(shù)值不同斩熊。

解決:要想將父類對(duì)象也序列化伐庭,就需要讓父類也實(shí)現(xiàn)Serializable 接口。如果父類不實(shí)現(xiàn)的話的霸株,就 需要有默認(rèn)的無參的構(gòu)造函數(shù)集乔。在父類沒有實(shí)現(xiàn) Serializable 接口時(shí),
虛擬機(jī)是不會(huì)序列化父對(duì)象的箫攀,而一個(gè) Java 對(duì)象的構(gòu)造必須先有父對(duì)象幼衰,才有子對(duì)象,反序列化也不例外渡嚣。所以反序列化時(shí)识椰,為了構(gòu)造父對(duì)象,
只能調(diào)用父類的無參構(gòu)造函數(shù)作為默認(rèn)的父對(duì)象腹鹉。因此當(dāng)我們?nèi)「笇?duì)象的變量值時(shí),它的值是調(diào)用父類無參構(gòu)造函數(shù)后的值愉阎。如果你考慮到這種序列化的情況力奋,
在父類無參構(gòu)造函數(shù)中對(duì)變量進(jìn)行初始化,否則的話溅呢,父類變量值都是默認(rèn)聲明的值,如 int 型的默認(rèn)是 0驶鹉,string 型的默認(rèn)是 null铣墨。

Transient 關(guān)鍵字的作用是控制變量的序列化,在變量聲明前加上該關(guān)鍵字词顾,可以阻止該變量被序列化到文件中碱妆,在被反序列化后,transient 變量的值被設(shè)為初始值疹尾,
如 int 型的是 0,對(duì)象型的是 null窍蓝。

特性使用案例

我們熟悉使用 Transient 關(guān)鍵字可以使得字段不被序列化繁成,那么還有別的方法嗎?<font color=#0099ff>根據(jù)父類對(duì)象序列化的規(guī)則面睛,我們可以將不需要被序列化的字段抽取出來放到父類中尊搬,
子類實(shí)現(xiàn) Serializable 接口,父類不實(shí)現(xiàn)幌墓,根據(jù)父類序列化規(guī)則冀泻,父類的字段數(shù)據(jù)將不被序列化,不用重復(fù)抒寫 transient袭祟,代碼簡(jiǎn)潔捞附。</font>

對(duì)敏感字段加密

情境:服務(wù)器端給客戶端發(fā)送序列化對(duì)象數(shù)據(jù),對(duì)象中有一些數(shù)據(jù)是敏感的胆绊,比如密碼字符串等欧募,希望對(duì)該密碼字段在序列化時(shí),進(jìn)行加密种冬,
而客戶端如果擁有解密的密鑰舔糖,只有在客戶端進(jìn)行反序列化時(shí),才可以對(duì)密碼進(jìn)行讀取金吗,這樣可以一定程度保證序列化對(duì)象的數(shù)據(jù)安全摇庙。

解決:在序列化過程中,虛擬機(jī)會(huì)試圖調(diào)用對(duì)象類里的 writeObjectreadObject 方法卫袒,進(jìn)行用戶自定義的序列化和反序列化夕凝,如果沒有這樣的方法,
則默認(rèn)調(diào)用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法讽营。
用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程泡徙,比如可以在序列化的過程中動(dòng)態(tài)改變序列化的數(shù)值。
基于這個(gè)原理堪藐,可以在實(shí)際應(yīng)用中得到使用礁竞,用于敏感字段的加密工作,清單 3 展示了這個(gè)過程模捂。
清單 3. 靜態(tài)變量序列化問題代碼

 private static final long serialVersionUID = 1L;

    private String password = "pass";

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    private void writeObject(ObjectOutputStream out) {
        try {
            PutField putFields = out.putFields();
            System.out.println("原密碼:" + password);
            password = "encryption";//模擬加密
            putFields.put("password", password);
            System.out.println("加密后的密碼" + password);
            out.writeFields();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void readObject(ObjectInputStream in) {
        try {
            GetField readFields = in.readFields();
            Object object = readFields.get("password", "");
            System.out.println("要解密的字符串:" + object.toString());
            password = "pass";//模擬解密,需要獲得本地的密鑰
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        try {
            ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream("result.obj"));
            out.writeObject(new Test());
            out.close();

            ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
                    "result.obj"));
            Test t = (Test) oin.readObject();
            System.out.println("解密后的字符串:" + t.getPassword());
            oin.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

在清單 3 的 writeObject 方法中综看,對(duì)密碼進(jìn)行了加密,在 readObject 中則對(duì) password 進(jìn)行解密舞吭,只有擁有密鑰的客戶端析珊,才可以正確的解析出密碼,確保了數(shù)據(jù)的安全忠寻。

特性使用案例

RMI 技術(shù)是完全基于 Java 序列化技術(shù)的锡溯,服務(wù)器端接口調(diào)用所需要的參數(shù)對(duì)象來至于客戶端,它們通過網(wǎng)絡(luò)相互傳輸祭饭。
這就涉及 RMI 的安全傳輸?shù)膯栴}倡蝙。一些敏感的字段,如用戶名密碼(用戶登錄時(shí)需要對(duì)密碼進(jìn)行傳輸)寺鸥,我們希望對(duì)其進(jìn)行加密,
這時(shí)烤低,就可以采用本節(jié)介紹的方法在客戶端對(duì)密碼進(jìn)行加密笆载,服務(wù)器端進(jìn)行解密,確保數(shù)據(jù)傳輸?shù)陌踩浴?/p>

序列化存儲(chǔ)規(guī)則

情境:?jiǎn)栴}代碼如清單 4 所示腻要。
清單 4. 存儲(chǔ)規(guī)則問題代碼

 ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream("result.obj"));
    Test test = new Test();
    //試圖將對(duì)象兩次寫入文件
    out.writeObject(test);
    out.flush();
    System.out.println(new File("result.obj").length());
    out.writeObject(test);
    out.close();
    System.out.println(new File("result.obj").length());

    ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
            "result.obj"));
    //從文件依次讀出兩個(gè)文件
    Test t1 = (Test) oin.readObject();
    Test t2 = (Test) oin.readObject();
    oin.close();
            
    //判斷兩個(gè)引用是否指向同一個(gè)對(duì)象
    System.out.println(t1 == t2);

清單 4 中對(duì)同一對(duì)象兩次寫入文件雄家,打印出寫入一次對(duì)象后的存儲(chǔ)大小和寫入兩次后的存儲(chǔ)大小胀滚,然后從文件中反序列化出兩個(gè)對(duì)象乱投,
比較這兩個(gè)對(duì)象是否為同一對(duì)象咙好。一般的思維是褐荷,兩次寫入對(duì)象,文件大小會(huì)變?yōu)閮杀兜拇笮〔愎葱蛄谢瘯r(shí)其监,由于從文件讀取,生成了兩個(gè)對(duì)象毁菱,
判斷相等時(shí)應(yīng)該是輸入 false 才對(duì)锌历,但是第二次寫入對(duì)象時(shí)文件只增加了 5 字節(jié),并且兩個(gè)對(duì)象是相等的窗慎,這是為什么呢卤材?

解答:Java 序列化機(jī)制為了節(jié)省磁盤空間,具有特定的存儲(chǔ)規(guī)則术吗,當(dāng)寫入文件的為同一對(duì)象時(shí)帆精,并不會(huì)再將對(duì)象的內(nèi)容進(jìn)行存儲(chǔ),而只是再次存儲(chǔ)一份引用吝镣,
上面增加的 5 字節(jié)的存儲(chǔ)空間就是新增引用和一些控制信息的空間昆庇。反序列化時(shí),恢復(fù)引用關(guān)系拱撵,使得清單 4 中的 t1 和 t2 指向唯一的對(duì)象,二者相等拴测,輸出 true。該存儲(chǔ)規(guī)則極大的節(jié)省了存儲(chǔ)空間屿愚。

特性案例分析

查看清單 5 的代碼务荆。
清單 5. 案例代碼

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
Test test = new Test();
test.i = 1;
out.writeObject(test);
out.flush();
test.i = 2;
out.writeObject(test);
out.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
                    "result.obj"));
Test t1 = (Test) oin.readObject();
Test t2 = (Test) oin.readObject();
System.out.println(t1.i);
System.out.println(t2.i);

清單 5 的目的是希望將 test 對(duì)象兩次保存到 result.obj 文件中函匕,寫入一次以后修改對(duì)象屬性值再次保存第二次,然后從 result.obj 中再依次讀出兩個(gè)對(duì)象中剩,
輸出這兩個(gè)對(duì)象的 i 屬性值抒寂。案例代碼的目的原本是希望一次性傳輸對(duì)象修改前后的狀態(tài)。

結(jié)果兩個(gè)輸出的都是 1蓬推, 原因就是第一次寫入對(duì)象以后沸伏,第二次再試圖寫的時(shí)候,虛擬機(jī)根據(jù)引用關(guān)系知道已經(jīng)有一個(gè)相同對(duì)象已經(jīng)寫入文件红选,
因此只保存第二次寫的引用姆另,所以讀取時(shí),都是第一次保存的對(duì)象蝶防。讀者在使用一個(gè)文件多次 writeObject 需要特別注意這個(gè)問題明吩。

小結(jié)

本文通過幾個(gè)具體的情景,介紹了 Java 序列化的一些高級(jí)知識(shí),雖說高級(jí)详羡,并不是說讀者們都不了解嘿悬,希望用筆者介紹的情景讓讀者加深印象,
能夠更加合理的利用 Java 序列化技術(shù)窒盐,在未來開發(fā)之路上遇到序列化問題時(shí)躯概,可以及時(shí)的解決娶靡。由于本人知識(shí)水平有限看锉,文章中倘若有錯(cuò)誤的地方,歡迎聯(lián)系我批評(píng)指正伯铣。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末腔寡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子忿磅,更是在濱河造成了極大的恐慌凭语,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吨些,死亡現(xiàn)場(chǎng)離奇詭異炒辉,居然都是意外死亡黔寇,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門状囱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人亭枷,你說我怎么就攤上這事∵墩常” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)驴党。 經(jīng)常有香客問我,道長(zhǎng)倔既,這世上最難降的妖魔是什么鹏氧? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任把还,我火速辦了婚禮,結(jié)果婚禮上吊履,老公的妹妹穿的比我還像新娘率翅。我一直安慰自己,他們只是感情好冕臭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布辜贵。 她就那樣靜靜地躺著,像睡著了一般鼻由。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蕉世,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天狠轻,我揣著相機(jī)與錄音,去河邊找鬼查吊。 笑死湖蜕,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的评也。 我是一名探鬼主播戈鲁,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼婆殿,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼罩扇!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起消约,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤员帮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后氯材,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體硝岗,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡型檀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了裂七。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡悬蔽,死狀恐怖蝎困,靈堂內(nèi)的尸體忽然破棺而出倍啥,到底是詐尸還是另有隱情,我是刑警寧澤虽缕,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布氮趋,位于F島的核電站,受9級(jí)特大地震影響剩胁,放射性物質(zhì)發(fā)生泄漏昵观。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一灼擂、第九天 我趴在偏房一處隱蔽的房頂上張望觉至。 院中可真熱鬧,春花似錦峻贮、人聲如沸沃暗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至唬涧,卻和暖如春疫赎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背碎节。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工捧搞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狮荔。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓胎撇,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親殖氏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子晚树,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 一、 序列化和反序列化概念 Serialization(序列化)是一種將對(duì)象以一連串的字節(jié)描述的過程雅采;反序列化de...
    步積閱讀 1,441評(píng)論 0 10
  • JAVA序列化機(jī)制的深入研究 對(duì)象序列化的最主要的用處就是在傳遞,和保存對(duì)象(object)的時(shí)候,保證對(duì)象的完整...
    時(shí)待吾閱讀 10,862評(píng)論 0 24
  • 序列化和反序列化的概念 序列化:把java對(duì)象轉(zhuǎn)換為字節(jié)序列的過程稱為對(duì)象的序列化,這些字節(jié)序列可以被保存在磁盤上...
    snoweek閱讀 702評(píng)論 0 3
  • 序列化的意義 1.永久存儲(chǔ)某個(gè)jvm中運(yùn)行時(shí)的對(duì)象。2.對(duì)象可以網(wǎng)絡(luò)傳輸3.rmi調(diào)用都是以序列化的方式傳輸參數(shù) ...
    炫邁哥閱讀 654評(píng)論 0 0
  • 簡(jiǎn)介 對(duì)于一個(gè)存在于Java虛擬機(jī)中的對(duì)象來說婚瓜,其內(nèi)部的狀態(tài)只保持在內(nèi)存中宝鼓。JVM停止之后,這些狀態(tài)就丟失了巴刻。在很...
    FX_SKY閱讀 793評(píng)論 0 0