面試官:您能說說序列化和反序列化嗎?是怎么實現(xiàn)的?什么場景下需要它?


作者:xcbeyond
瘋狂源自夢想,技術(shù)成就輝煌!微信公眾號:《程序猿技術(shù)大咖》號主,專注后端開發(fā)多年,擁有豐富的研發(fā)經(jīng)驗,樂于技術(shù)輸出、分享,現(xiàn)階段從事微服務(wù)架構(gòu)項目的研發(fā)工作,涉及架構(gòu)設(shè)計、技術(shù)選型、業(yè)務(wù)研發(fā)等工作。對于Java、微服務(wù)、數(shù)據(jù)庫、Docker有深入了解,并有大量的調(diào)優(yōu)經(jīng)驗。 






  

序列化和反序列化是Java中最基礎(chǔ)的知識點,也是很容易被大家遺忘的,雖然天天使用它,但并不一定都能清楚的說明白。我相信很多小伙伴們掌握的也就幾句概念、關(guān)鍵字(Serializable)而已,如果深究問一下序列化和反序列化是如何實現(xiàn)、使用場景等,就可能不知所措了。在每次我作為面試官,考察Java基礎(chǔ)時,通常都會問到序列化、反序列化的知識點,用以衡量其Java基礎(chǔ)如何。當被問及Java序列化是什么?反序列化是什么?什么場景下會用到?如果不用它,會出現(xiàn)什么問題等,一般大家回答也就是幾句簡單的概念而已,有的工作好幾年的應(yīng)聘者甚至連概念都說不清楚,一臉悶逼。

    本文就序列化和反序列化展開深入的探討,當被別人問及時,不至于一臉悶逼、尷尬,或許會為你以后的求職面試中增加一點點籌碼。

一、基本概念

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

   序列化是指將Java對象轉(zhuǎn)換為字節(jié)序列的過程,而反序列化則是將字節(jié)序列轉(zhuǎn)換為Java對象的過程。

  Java對象序列化是將實現(xiàn)了Serializable接口的對象轉(zhuǎn)換成一個字節(jié)序列,能夠通過網(wǎng)絡(luò)傳輸、文件存儲等方式傳輸 ,傳輸過程中卻不必擔心數(shù)據(jù)在不同機器、不同環(huán)境下發(fā)生改變,也不必關(guān)心字節(jié)的順序或其他任何細節(jié),并能夠在以后將這個字節(jié)序列完全恢復為原來的對象(恢復這一過程稱之為反序列化)。

  對象的序列化是非常有趣的,因為利用它可以實現(xiàn)輕量級持久性,“持久性”意味著一個對象的生存周期不單單取決于程序是否正在運行,它可以生存于程序的調(diào)用之間。通過將一個序列化對象寫入磁盤,然后在重新調(diào)用程序時恢復該對象,從而達到實現(xiàn)對象的持久性的效果。

 本質(zhì)上講,序列化就是把實體對象狀態(tài)按照一定的格式寫入到有序字節(jié)流,反序列化就是從有序字節(jié)流重建對象,恢復對象狀態(tài)。

2、為什么需要使用序列化和反序列化

  我們知道,不同進程/程序間進行遠程通信時,可以相互發(fā)送各種類型的數(shù)據(jù),包括文本、圖片、音頻、視頻等,而這些數(shù)據(jù)都會以二進制序列的形式在網(wǎng)絡(luò)上傳送。

  那么當兩個Java進程進行通信時,能否實現(xiàn)進程間的對象傳送呢?當然是可以的!如何做到呢?這就需要使用Java序列化與反序列化了。發(fā)送方需要把這個Java對象轉(zhuǎn)換為字節(jié)序列,然后在網(wǎng)絡(luò)上傳輸,接收方則需要將字節(jié)序列中恢復出Java對象。

 我們清楚了為什么需要使用Java序列化和反序列化后,我們很自然地會想到Java序列化有哪些好處:

實現(xiàn)了數(shù)據(jù)的持久化,通過序列化可以把數(shù)據(jù)永久地保存到硬盤上(如:存儲在文件里),實現(xiàn)永久保存對象。
利用序列化實現(xiàn)遠程通信,即:能夠在網(wǎng)絡(luò)上傳輸對象。

二、如何實現(xiàn)Java序列化和反序列化

  只要對象實現(xiàn)了Serializable、Externalizable接口(該接口僅僅是一個標記接口,并不包含任何方法),則該對象就實現(xiàn)了序列化。
  • 1

1、具體是如何實現(xiàn)的呢?

 序列化,首先要創(chuàng)建某些OutputStream對象,然后將其封裝在一個ObjectOutputStream對象內(nèi),這時調(diào)用writeObject()方法,即可將對象序列化,并將其發(fā)送給OutputStream(對象序列化是基于字節(jié)的,因此使用的InputStream和OutputStream繼承的類)。
















 反序列化,即反向進行序列化的過程,需要將一個InputStream封裝在ObjectInputStream對象內(nèi),然后調(diào)用readObject()方法,獲得一個對象引用(它是指向一個向上轉(zhuǎn)型的Object),然后進行類型強制轉(zhuǎn)換來得到該對象。

在這里插入圖片描述


















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

(1)若User類僅僅實現(xiàn)了Serializable接口,則可以按照以下方式進行序列化和反序列化。

 ObjectOutputStream采用默認的序列化方式,對User對象的非transient的實例變量進行序列化。
 ObjcetInputStream采用默認的反序列化方式,對對User對象的非transient的實例變量進行反序列化。

(2)若User類僅僅實現(xiàn)了Serializable接口,并且還定義了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),則采用以下方式進行序列化與反序列化。

ObjectOutputStream調(diào)用User對象的writeObject(ObjectOutputStream out)的方法進行序列化。
ObjectInputStream會調(diào)用User對象的readObject(ObjectInputStream in)的方法進行反序列化。

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

ObjectOutputStream調(diào)用User對象的writeExternal(ObjectOutput out))的方法進行序列化。
ObjectInputStream會調(diào)用User對象的readExternal(ObjectInput in)的方法進行反序列化。

    java.io.ObjectOutputStream:對象輸出流,它的writeObject(Object obj)方法可以對指定的obj對象進行序列化,把得到的字節(jié)序列寫到一個目標輸出流中。
    java.io.ObjectInputStream:對象輸入流,它的readObject()方法可以將從輸入流中讀取字節(jié)序列,再把它們反序列化成為一個對象,并將其返回。

2、序列化和反序列化示例

  為了更好的理解序列化和反序列化的過程,舉例如下:

public class SerialDemo {
 
    public static void main(String[] args) throws IOException, ClassNotFoundException {
	// 序列化對象User
        FileOutputStream fos = new FileOutputStream("object.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        User user1 = new User("xcbeyond", "123456789");
        oos.writeObject(user1);
        oos.flush();
        oos.close();
 
	// 反序列化
        FileInputStream fis = new FileInputStream("object.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        User user2 = (User) ois.readObject();
        System.out.println(user2.getUsername()+ "," + user2.getPassword());
    }
}

// 對象User,對其實現(xiàn)了Serializable接口
public class User implements Serializable {
    private String username;
    private String password;
    
    ……
 
}

3、什么場景下需要序列化

當你想把的內(nèi)存中的對象狀態(tài)保存到一個文件中或者數(shù)據(jù)庫中時候。
當你想用套接字在網(wǎng)絡(luò)上傳送對象的時候。
當你想通過RMI傳輸對象的時候。

三、注意事項

1、當一個父類實現(xiàn)序列化,子類就會自動實現(xiàn)序列化,不需要顯式實現(xiàn)Serializable接口。

2、當一個對象的實例變量引用其他對象,序列化該對象時也把引用對象進行序列化。

3、并非所有的對象都可以進行序列化,比如:

  安全方面的原因,比如一個對象擁有private,public等成員變量,對于一個要傳輸?shù)膶ο螅热鐚懙轿募蛘哌M行RMI傳輸?shù)鹊?,在序列化進行傳輸?shù)倪^程中,這個對象的private等域是不受保護的;

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

4、聲明為static和transient類型的成員變量不能被序列化。因為static代表類的狀態(tài),transient代表對象的臨時數(shù)據(jù)。

5、序列化運行時會使用一個稱為 serialVersionUID 的版本號,并與每個可序列化的類相關(guān)聯(lián),該序列號在反序列化過程中用于驗證序列化對象的發(fā)送者和接收者是否為該對象加載了與序列化兼容的類。如果接收者加載的該對象的類的 serialVersionUID 與對應(yīng)的發(fā)送者的類的版本號不同,則反序列化將會導致 InvalidClassException。可序列化類可以通過聲明名為 “serialVersionUID” 的字段(該字段必須是靜態(tài) (static)、最終 (final) 的 long 型字段)顯式聲明其自己的 serialVersionUID。

  如果序列化的類未顯式的聲明 serialVersionUID,則序列化運行時將基于該類的各個方面計算該類的默認 serialVersionUID 值,如“Java(TM) 對象序列化規(guī)范”中所述。不過,強烈建議 所有可序列化類都顯式聲明 serialVersionUID 值,原因是計算默認的 serialVersionUID 對類的詳細信息具有較高的敏感性,根據(jù)編譯器實現(xiàn)的不同可能千差萬別,這樣在反序列化過程中可能會導致意外的 InvalidClassException。因此,為保證 serialVersionUID 值跨不同 java 編譯器實現(xiàn)的一致性,序列化類必須聲明一個明確的 serialVersionUID 值。還強烈建議使用 private 修飾符顯示聲明 serialVersionUID(如果可能),原因是這種聲明僅應(yīng)用于直接聲明類 -- serialVersionUID 字段作為繼承成員沒有用處。數(shù)組類不能聲明一個明確的 serialVersionUID,因此它們總是具有默認的計算值,但是數(shù)組類沒有匹配 serialVersionUID 值的要求。
  • 1

6、Java有很多基礎(chǔ)類已經(jīng)實現(xiàn)了serializable接口,比如String,Vector等。但是也有一些沒有實現(xiàn)serializable接口的。

7、如果一個對象的成員變量是一個對象,那么這個對象的數(shù)據(jù)成員也會被保存!這是能用序列化解決深拷貝的重要原因。

 有了上面關(guān)于序列化和反序列化的詳細介紹,現(xiàn)在你對平時所用的序列化和反序列化是如何實現(xiàn)的,什么場景下會使用它,是不是更加深刻了吧
  • 1

參考:

(美) Bruce Eckel 著 陳昊鵬 譯 《Java編程思想》
https://blog.csdn.net/xlgen157387/article/details/79840134
https://blog.csdn.net/Mr_EvanChen/article/details/79724426