序列化與反序列化

引言

將 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),
通過(guò)分析情境出現(xiàn)的原因,使讀者輕松牢記 Java 序列化中的一些高級(jí)認(rèn)識(shí)。

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

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

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

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

序列化 ID 問(wèn)題

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

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

當(dāng)實(shí)現(xiàn)java.io.Serializable接口的實(shí)體(類)沒(méi)有顯式地定義一個(gè)名為serialVersionUID,類型為long的變量時(shí),
Java序列化機(jī)制會(huì)根據(jù)編譯的class自動(dòng)生成一個(gè)serialVersionUID作序列化版本比較用,這種情況下,只有同一次編譯生成的class才會(huì)生成相同的serialVersionUID 。
如果我們不希望通過(guò)編譯來(lái)強(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è)建議,
如果沒(méi)有特殊需求,就是用默認(rèn)的 1L 就可以,這樣可以確保代碼一致時(shí)反序列化成功。那么隨機(jī)生成的序列化 ID 有什么作用呢,有些時(shí)候,通過(guò)改變序列化 ID 可以用來(lái)限制某些用戶的使用。

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

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

 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();
            
            //再讀取,通過(guò)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ì)象讀取出來(lái),然后通過(guò)讀取出來(lái)的對(duì)象獲得靜態(tài)變量的數(shù)值并打印出來(lái)。依照清單 2,
這個(gè) System.out.println(t.staticVar) 語(yǔ)句輸出的是 10 還是 5 呢?

最后的輸出是 10,對(duì)于無(wú)法理解的讀者認(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 接口,它的父類都沒(méi)有實(shí)現(xiàn) Serializable 接口,序列化該子類對(duì)象,然后反序列化后輸出父類定義的某變量的數(shù)值,該變量數(shù)值與序列化時(shí)的數(shù)值不同。

解決:要想將父類對(duì)象也序列化,就需要讓父類也實(shí)現(xiàn)Serializable 接口。如果父類不實(shí)現(xiàn)的話的,就 需要有默認(rèn)的無(wú)參的構(gòu)造函數(shù)。在父類沒(méi)有實(shí)現(xiàn) Serializable 接口時(shí),
虛擬機(jī)是不會(huì)序列化父對(duì)象的,而一個(gè) Java 對(duì)象的構(gòu)造必須先有父對(duì)象,才有子對(duì)象,反序列化也不例外。所以反序列化時(shí),為了構(gòu)造父對(duì)象,
只能調(diào)用父類的無(wú)參構(gòu)造函數(shù)作為默認(rèn)的父對(duì)象。因此當(dāng)我們?nèi)「笇?duì)象的變量值時(shí),它的值是調(diào)用父類無(wú)參構(gòu)造函數(shù)后的值。如果你考慮到這種序列化的情況,
在父類無(wú)參構(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ī)則,我們可以將不需要被序列化的字段抽取出來(lái)放到父類中,
子類實(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ù)安全。

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

 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ì)象來(lái)至于客戶端,它們通過(guò)網(wǎng)絡(luò)相互傳輸。
這就涉及 RMI 的安全傳輸?shù)膯?wèn)題。一些敏感的字段,如用戶名密碼(用戶登錄時(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ī)則問(wèn)題代碼

 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)閮杀兜拇笮?,反序列化時(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è)問(wèn)題。

小結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容