Java 的序列化和反序列化機(jī)制

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

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

2. 序列化 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)行串行化和反串行化聂使。

package com.inout; 

import java.io.Serializable; 

public class A implements Serializable { 

    private static final long serialVersionUID = 1L; 

    private String name; 
    
    public String getName() { 
            return name; 
    } 
    
    public void setName(String name) { 
        this.name = name; 
    } 
} 

package com.inout; 

import java.io.Serializable; 

public class A implements Serializable { 

     private static final long serialVersionUID = 2L; 
    
     private String name; 
    
    public String getName() { 
        return name; 
    } 
    
    public void setName(String name) { 
        this.name = name; 
    } 
}

3. 靜態(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();
        }
    }
}

清單 2 中的 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)變量捍靠。

4. 父類的序列化與 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。

代碼范例:

class TestFather {

    String name;
}

class TestChild extends TestFather implements Serializable {

    private static final long serialVersionUID = -5085120126097209576L;

    int age = 30;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setFatherName(String name) {
        super.name = name;
    }

    public String getFatherName() {
        return super.name;
    }

}

    public static void main(String[] args) throws Exception {
        TestChild c = new TestChild();
        c.setFatherName("abc");
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test1.obj"));
        oos.writeObject(c);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test1.obj"));
        TestChild cc = (TestChild) ois.readObject();
        System.out.println(cc.getFatherName());
    }

結(jié)果輸出

null

特性使用案例

我們熟悉使用 Transient 關(guān)鍵字可以使得字段不被序列化绒疗,那么還有別的方法嗎侵歇?根據(jù)父類對(duì)象序列化的規(guī)則,我們可以將不需要被序列化的字段抽取出來放到父類中吓蘑,子類實(shí)現(xiàn) Serializable 接口惕虑,父類不實(shí)現(xiàn),根據(jù)父類序列化規(guī)則,父類的字段數(shù)據(jù)將不被序列化溃蔫,形成類圖如圖 2 所示健提。


image
image

5. 對(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ì)象類里的 writeObject 和 readObject 方法辉哥,進(jìn)行用戶自定義的序列化和反序列化,如果沒有這樣的方法攒射,則默認(rèn)調(diào)用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法醋旦。用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動(dòng)態(tài)改變序列化的數(shù)值会放∷瞧耄基于這個(gè)原理,可以在實(shí)際應(yīng)用中得到使用咧最,用于敏感字段的加密工作捂人,清單 3 展示了這個(gè)過程。

class PasswordObj implements Serializable{
    private static final long serialVersionUID = 3847029983123556071L;

    private String password = "password";

    public String getPassword() {
        return password;
    }

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

    //序列化時(shí)對(duì)密碼加密
    private void writeObject(ObjectOutputStream stream) throws IOException {
        System.out.println("原始密碼: " + password);
        password = "encryption";
        System.out.println("加密后的密碼: " + password);
        stream.writeObject(password);

    }

    //反序列化時(shí)解密
    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
        String s = (String) stream.readObject();
        System.out.println("讀取到的密碼: " + s);
        password = "password";

    }
}

6. 序列化存儲(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);

對(duì)同一對(duì)象兩次寫入文件矢沿,打印出寫入一次對(duì)象后的存儲(chǔ)大小和寫入兩次后的存儲(chǔ)大小滥搭,然后從文件中反序列化出兩個(gè)對(duì)象,比較這兩個(gè)對(duì)象是否為同一對(duì)象捣鲸。一般的思維是瑟匆,兩次寫入對(duì)象,文件大小會(huì)變?yōu)閮杀兜拇笮≡曰蹋葱蛄谢瘯r(shí)愁溜,由于從文件讀取,生成了兩個(gè)對(duì)象媒役,判斷相等時(shí)應(yīng)該是輸入 false 才對(duì)祝谚,但是最后結(jié)果是 true

我們看到酣衷,第二次寫入對(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)系捐寥,使得清單 3 中的 t1 和 t2 指向唯一的對(duì)象,二者相等祖驱,輸出 true握恳。該存儲(chǔ)規(guī)則極大的節(jié)省了存儲(chǔ)空間。

特性案例分析

查看清單 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);

清單 4 的目的是希望將 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è)問題榛丢。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市挺庞,隨后出現(xiàn)的幾起案子晰赞,更是在濱河造成了極大的恐慌,老刑警劉巖选侨,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掖鱼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡援制,警方通過查閱死者的電腦和手機(jī)戏挡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來晨仑,“玉大人褐墅,你說我怎么就攤上這事拆檬。” “怎么了妥凳?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵竟贯,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我逝钥,道長(zhǎng)屑那,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任艘款,我火速辦了婚禮持际,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘磷箕。我一直安慰自己选酗,他們只是感情好阵难,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布岳枷。 她就那樣靜靜地躺著,像睡著了一般呜叫。 火紅的嫁衣襯著肌膚如雪空繁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天朱庆,我揣著相機(jī)與錄音盛泡,去河邊找鬼。 笑死娱颊,一個(gè)胖子當(dāng)著我的面吹牛傲诵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播箱硕,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼拴竹,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了剧罩?” 一聲冷哼從身側(cè)響起栓拜,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惠昔,沒想到半個(gè)月后幕与,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡镇防,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年啦鸣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片来氧。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡诫给,死狀恐怖饼齿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蝙搔,我是刑警寧澤缕溉,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站吃型,受9級(jí)特大地震影響证鸥,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜勤晚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一枉层、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧赐写,春花似錦鸟蜡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至端铛,卻和暖如春泣矛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背禾蚕。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工您朽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人换淆。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓哗总,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親倍试。 傳聞我的和親對(duì)象是個(gè)殘疾皇子讯屈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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

  • JAVA序列化機(jī)制的深入研究 對(duì)象序列化的最主要的用處就是在傳遞,和保存對(duì)象(object)的時(shí)候,保證對(duì)象的完整...
    時(shí)待吾閱讀 10,837評(píng)論 0 24
  • 官方文檔理解 要使類的成員變量可以序列化和反序列化,必須實(shí)現(xiàn)Serializable接口易猫。任何可序列化類的子類都是...
    獅_子歌歌閱讀 2,388評(píng)論 1 3
  • 一耻煤、 序列化和反序列化概念 Serialization(序列化)是一種將對(duì)象以一連串的字節(jié)描述的過程;反序列化de...
    步積閱讀 1,437評(píng)論 0 10
  • 如果你只知道實(shí)現(xiàn) Serializable 接口的對(duì)象准颓,可以序列化為本地文件哈蝇。那你最好再閱讀該篇文章,文章對(duì)序列化...
    jiangmo閱讀 456評(píng)論 0 2
  • 對(duì)象序列化的目標(biāo): 將對(duì)的字節(jié)序列對(duì)象永久的保存到磁盤中攘已。 允許在網(wǎng)絡(luò)上直接傳輸對(duì)象炮赦,傳輸對(duì)象的字節(jié)序列。 對(duì)象序...
    年少懵懂丶流年夢(mèng)閱讀 1,150評(píng)論 0 3