序列化和反序列化的底層實現原理是什麼?

JAVA攻城獅發表於2019-05-04

序列化和反序列化的底層實現原理是什麼?

前言

序列化和反序列化作為Java裡一個較為基礎的知識點,大家心裡也有那麼幾句要說的,但我相信很多小夥伴掌握的也就是那麼幾句而已,如果再深究問一下Java如何實現序列化和反序列化的,就可能不知所措了!遙記當年也被問了這一個問題,自信滿滿的說了一大堆,什麼是序列化、什麼是反序列化、什麼場景的時候才會用到等,然後面試官說:那你能說一下序列化和反序列化底層是如何實現的嗎?一臉懵逼,然後回家等通知!結果自然是涼了~

一、基本概念

1、什麼是序列化和反序列化

(1)Java序列化是指把Java物件轉換為位元組序列的過程,而Java反序列化是指把位元組序列恢復為Java物件的過程;

(2)序列化:物件序列化的最主要的用處就是在傳遞和儲存物件的時候,保證物件的完整性和可傳遞性。序列化是把物件轉換成有序位元組流,以便在網路上傳輸或者儲存在本地檔案中。序列化後的位元組流儲存了Java物件的狀態以及相關的描述資訊。序列化機制的核心作用就是物件狀態的儲存與重建。

(3)反序列化:客戶端從檔案中或網路上獲得序列化後的物件位元組流後,根據位元組流中所儲存的物件狀態及描述資訊,通過反序列化重建物件。

(4)本質上講,序列化就是把實體物件狀態按照一定的格式寫入到有序位元組流,反序列化就是從有序位元組流重建物件,恢復物件狀態。

2、為什麼需要序列化與反序列化

我們知道,當兩個程式進行遠端通訊時,可以相互傳送各種型別的資料,包括文字、圖片、音訊、視訊等, 而這些資料都會以二進位制序列的形式在網路上傳送。

那麼當兩個Java程式進行通訊時,能否實現程式間的物件傳送呢?答案是可以的!如何做到呢?這就需要Java序列化與反序列化了!

換句話說,一方面,傳送方需要把這個Java物件轉換為位元組序列,然後在網路上傳送;另一方面,接收方需要從位元組序列中恢復出Java物件。

當我們明晰了為什麼需要Java序列化和反序列化後,我們很自然地會想Java序列化的好處。其好處一是實現了資料的持久化,通過序列化可以把資料永久地儲存到硬碟上(通常存放在檔案裡),二是,利用序列化實現遠端通訊,即在網路上傳送物件的位元組序列。

總的來說可以歸結為以下幾點:

(1)永久性儲存物件,儲存物件的位元組序列到本地檔案或者資料庫中;

(2)通過序列化以位元組流的形式使物件在網路中進行傳遞和接收;

(3)通過序列化在程式間傳遞物件;

3、序列化演算法一般會按步驟做如下事情:

(1)將物件例項相關的類後設資料輸出。

(2)遞迴地輸出類的超類描述直到不再有超類。

(3)類後設資料完了以後,開始從最頂層的超類開始輸出物件例項的實際資料值。

(4)從上至下遞迴輸出例項的資料

二、Java如何實現序列化和反序列化

1、JDK類庫中序列化和反序列化API

(1)java.io.ObjectOutputStream:表示物件輸出流;

它的writeObject(Object obj)方法可以對引數指定的obj物件進行序列化,把得到的位元組序列寫到一個目標輸出流中;

(2)java.io.ObjectInputStream:表示物件輸入流;

它的readObject()方法源輸入流中讀取位元組序列,再把它們反序列化成為一個物件,並將其返回;

2、實現序列化的要求

只有實現了Serializable或Externalizable介面的類的物件才能被序列化,否則丟擲異常!

3、實現Java物件序列化與反序列化的方法

假定一個User類,它的物件需要序列化,可以有如下三種方法:

(1)若User類僅僅實現了Serializable介面,則可以按照以下方式進行序列化和反序列化

ObjectOutputStream採用預設的序列化方式,對User物件的非transient的例項變數進行序列化。

ObjcetInputStream採用預設的反序列化方式,對對User物件的非transient的例項變數進行反序列化。

(2)若User類僅僅實現了Serializable介面,並且還定義了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),則採用以下方式進行序列化與反序列化。

ObjectOutputStream呼叫User物件的writeObject(ObjectOutputStream out)的方法進行序列化。

ObjectInputStream會呼叫User物件的readObject(ObjectInputStream in)的方法進行反序列化。

(3)若User類實現了Externalnalizable介面,且User類必須實現readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,則按照以下方式進行序列化與反序列化。

ObjectOutputStream呼叫User物件的writeExternal(ObjectOutput out))的方法進行序列化。

ObjectInputStream會呼叫User物件的readExternal(ObjectInput in)的方法進行反序列化。

4、JDK類庫中序列化的步驟

步驟一:建立一個物件輸出流,它可以包裝一個其它型別的目標輸出流,如檔案輸出流:

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\object.out"));
複製程式碼

步驟二:通過物件輸出流的writeObject()方法寫物件:

oos.writeObject(new User("xuliugen", "123456", "male"));
複製程式碼

5、JDK類庫中反序列化的步驟

步驟一:建立一個物件輸入流,它可以包裝一個其它型別輸入流,如檔案輸入流:

ObjectInputStream ois= new ObjectInputStream(new FileInputStream("object.out"));
複製程式碼

步驟二:通過物件輸出流的readObject()方法讀取物件:

User user = (User) ois.readObject();
複製程式碼

說明:為了正確讀取資料,完成反序列化,必須保證向物件輸出流寫物件的順序與從物件輸入流中讀物件的順序一致。

6、序列化和反序列化的示例

為了更好地理解Java序列化與反序列化,舉一個簡單的示例如下:

public class SerialDemo {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //序列化
        FileOutputStream fos = new FileOutputStream("object.out");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        User user1 = new User("xuliugen", "123456", "male");
        oos.writeObject(user1);
        oos.flush();
        oos.close();
        //反序列化
        FileInputStream fis = new FileInputStream("object.out");
        ObjectInputStream ois = new ObjectInputStream(fis);
        User user2 = (User) ois.readObject();
        System.out.println(user2.getUserName()+ " " + 
            user2.getPassword() + " " + user2.getSex());
        //反序列化的輸出結果為:xuliugen 123456 male
    }
}

public class User implements Serializable {
    private String userName;
    private String password;
    private String sex;
    //全參構造方法、get和set方法省略
}
複製程式碼

object.out檔案如下(使用UltraEdit開啟):

序列化和反序列化的底層實現原理是什麼?

注:上圖中0000000h-000000c0h表示行號;0-f表示列;行後面的文字表示對這行16進位制的解釋;對上述位元組碼所表述的內容感興趣的可以對照相關的資料,查閱一下每一個字元代表的含義,這裡不在探討!

類似於我們Java程式碼編譯之後的.class檔案,每一個字元都代表一定的含義。序列化和反序列化的過程就是生成和解析上述字元的過程!

序列化圖示:

序列化和反序列化的底層實現原理是什麼?

反序列化圖示:

序列化和反序列化的底層實現原理是什麼?

三、相關注意事項

1、序列化時,只對物件的狀態進行儲存,而不管物件的方法;

2、當一個父類實現序列化,子類自動實現序列化,不需要顯式實現Serializable介面;

3、當一個物件的例項變數引用其他物件,序列化該物件時也把引用物件進行序列化;

4、並非所有的物件都可以序列化,至於為什麼不可以,有很多原因了,比如:

  • 安全方面的原因,比如一個物件擁有private,public等field,對於一個要傳輸的物件,比如寫到檔案,或者進行RMI傳輸等等,在序列化進行傳輸的過程中,這個物件的private等域是不受保護的;

  • 資源分配方面的原因,比如socket,thread類,如果可以序列化,進行傳輸或者儲存,也無法對他們進行重新的資源分配,而且,也是沒有必要這樣實現;

5、宣告為static和transient型別的成員資料不能被序列化。因為static代表類的狀態,transient代表物件的臨時資料。

6、序列化執行時使用一個稱為 serialVersionUID 的版本號與每個可序列化類相關聯,該序列號在反序列化過程中用於驗證序列化物件的傳送者和接收者是否為該物件載入了與序列化相容的類。為它賦予明確的值。顯式地定義serialVersionUID有兩種用途:

  • 在某些場合,希望類的不同版本對序列化相容,因此需要確保類的不同版本具有相同的serialVersionUID;

  • 在某些場合,不希望類的不同版本對序列化相容,因此需要確保類的不同版本具有不同的serialVersionUID。

7、Java有很多基礎類已經實現了serializable介面,比如String,Vector等。但是也有一些沒有實現serializable介面的;

8、如果一個物件的成員變數是一個物件,那麼這個物件的資料成員也會被儲存!這是能用序列化解決深拷貝的重要原因;

四、總結

看到這裡,可能已經讓我們很滿足了,畢竟已經知道了我們平時使用的序列化和反序列化是如何進行操作的,Java給我們提供了哪些介面可供使用,也比我們最初知道的簡單的什麼是序列化、反序列化以及作用多了很多!後續內容我們也會不斷再討論和更新!

相關文章