Java 中的類為什麼要實現序列化呢 / JAVA中序列化和反序列化中的靜態成員問題
阿新 • • 發佈:2019-02-06
很多人覺得自己寫得 Java 程式碼中,新建的 pojo 物件要實現序列化是為了要儲存到硬碟上,其實呢,實現序列化和儲存到硬碟上沒有必然的關係。
以下圖舉例:
假設左邊的是你的電腦,也就是客戶端,右邊的是伺服器。之前你的客戶端和伺服器可能都在同一個電腦上,都是 Windows 下,那麼右邊的伺服器也可以放到 Linux 中,這就涉及到左右兩個不同的伺服器了。中間用一條豎線分隔一下。
客戶端可以呼叫伺服器,所以肯定要傳遞引數。假設你傳遞的是字串,沒有問題,所有的機器都可以識別正常的字串。
那麼現在假設你傳遞的引數是一個 Java 物件,比如叫 cat。伺服器並沒有那麼智慧,它並不會知道你傳遞的是一個 Java 物件,而不是其他型別的資料,它識別不了 Java 物件。
Java 物件本質上是 class 位元組碼,伺服器並不能根據這個位元組碼識別出該 Java 物件。所以,要提供一個公共的格式,不僅 Windows 能識別,你的伺服器也能識別的公共的格式。
我們將 Java 物件轉換成公共的格式叫做序列化,將公共的格式轉換成物件叫做反序列化。儲存到磁碟只是序列化的一種表現形式。
就這麼簡單,小小的問題,希望對大家有所幫助。
學習過程中遇到什麼問題或者想獲取學習資源的話,歡迎加入Java學習交流群346942462,我們一起學Java!
JAVA中序列化和反序列化中的靜態成員問題
關於這個標題的內容是面試筆試中比較常見的考題,大家跟隨我的部落格一起來學習下這個過程。
JAVA中的序列化和反序列化主要用於: (1)將物件或者異常等寫入檔案,通過檔案互動傳輸資訊; (2)將物件或者異常等通過網路進行傳輸。 那麼為什麼需要序列化和反序列化呢?簡單來說,如果你只是自己同一臺機器的同一個環境下使用同一個JVM來操作,序列化和反序列化是沒必要的,當需要進行資料傳輸的時候就顯得十分必要。比如你的資料寫到檔案裡要被其他人的電腦的程式使用,或者你電腦上的資料需要通過網路傳輸給其他人的程式使用,像伺服器客戶端的這種模型就是一種應用,這個時候,大家想想,每個人的電腦配置可能不同,執行環境可能也不同,位元組序可能也不同,總之很多地方都不能保證一致,所以為了統一起見,我們傳輸的資料或者經過檔案儲存的資料需要經過序列化和編碼等操作,相當於互動雙方有一個公共的標準,按照這種標準來做,不管各自的環境是否有差異,各自都可以根據這種標準來翻譯出自己能理解的正確的資料。 在JAVA中有專門用於此類操作的API,供開發者直接使用,物件的序列化和反序列化可以通過將物件實現Serializable介面,然後用物件的輸入輸出流進行讀寫,下面看一個完整的例子。上面這段程式是定義了要被序列化和反序列化的類DataObject,這個類實現了Serializable介面,裡面有幾點需要注意: (1)類中有一個靜態成員變數i,這個變數能不能被序列化呢?等下通過測試程式看一下; (2)類中重寫了toString方法,是為了列印結果。 接下來我們看一下測試該類的物件序列化和反序列化的一個測試程式版本,提前說明,這個版本是有問題的。
- package
test2;- import java.io.Serializable;
- publicclass DataObject implements Serializable {
- /**
- * 序列化的UID號
- */
- privatestaticfinallong serialVersionUID = -3737338076212523007L;
- publicstaticint i = 0;
- private String word = "";
- publicstaticvoid setI(int i){
- DataObject.i = i;
- }
- publicvoid setWord(String word){
- this.word = word;
- }
- publicstaticint getI() {
- return i;
- }
- public String getWord() {
- return word;
- }
- @Override
- public String toString() {
- return"word = " + word + ", " + "i = " + i;
- }
- }
上面這段程式大家可以直接執行。注意,這裡定義了兩個方法Serialize()和Deserialize(),分別實現了序列化和反序列化的功能,裡面的主要用到了物件輸入輸出流和檔案輸入輸出流,大家看一下程式中的註釋就可以理解。在序列化的方法中,將物件的成員變數word設定成了"123",i設定成了"2",注意這裡的i是靜態變數,那麼以通常的序列化和反序列化的理解來看,無非就是一個正過程和一個逆過程,最終經過反序列化後,輸出物件中的word和i時,大家一般都覺得應該還是"123"和"2",那麼上面程式的執行結果確實就是:
- package test2;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.FileNotFoundException;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.ObjectInputStream;
- import java.io.ObjectOutputStream;
- /**
- * Description: 測試物件的序列化和反序列
- */
- publicclass TestObjSerializeAndDeserialize {
- publicstaticvoid main(String[] args) throws Exception {
- // 序列化DataObject物件
- Serialize();
- // 反序列DataObject物件
- DataObject object = Deserialize();
- // 靜態成員屬於類級別的,所以不能序列化,序列化只是序列化了物件而已,
- // 這裡的不能序列化的意思,是序列化資訊中不包含這個靜態成員域,下面
- // 之所以i輸出還是2,是因為測試都在同一個機器(而且是同一個程序),因為這個jvm
- // 已經把i載入進來了,所以獲取的是載入好的i,如果是傳到另一臺機器或者關掉程式重新
- // 寫個程式讀入DataObject.txt,此時因為別的機器或新的程序是重新載入i的,所以i資訊就是初始時的資訊,即0
- System.out.println(object);
- }
- /**
- * MethodName: SerializePerson
- * Description: 序列化Person物件
- * @author
- * @throws FileNotFoundException
- * @throws IOException
- */
- privatestaticvoid Serialize() throws FileNotFoundException, IOException {
- DataObject object = new DataObject();
- object.setWord("123");
- object.setI(2);
- // 建立ObjectOutputStream物件輸出流,其中用到了檔案的描述符物件和檔案輸出流物件
- ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(
- new File("DataObject.txt")));
- // 將DataObject物件儲存到DataObject.txt檔案中,完成對DataObject物件的序列化操作
- oo.writeObject(object);
- System.out.println("Person物件序列化成功!");
- // 最後一定記得關閉物件描述符!!!
- oo.close();
- }
- /**
- * MethodName: DeserializePerson
- * Description: 反序列DataObject物件
- * @author
- * @return
- * @throws Exception
- * @throws IOException
- */
- privatestatic DataObject Deserialize() throws Exception, IOException {
- // 建立ObjectInputStream物件輸入流,其中用到了檔案的描述符物件和檔案輸入流物件
- ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
- new File("DataObject.txt")));
- // 從DataObject.txt檔案中讀取DataObject物件,完成對DataObject物件的反序列化操作
- DataObject object = (DataObject) ois.readObject();
- System.out.println("Person物件反序列化成功!");
- // 最後一定記得關閉物件描述符!!!
- ois.close();
- return object;
- }
- }
這樣會使得大家覺得理應就是如此,其實這是錯誤的。大家要記住: 靜態成員屬於類級別的,所以不能序列化,序列化只是序列化了物件而已,這裡“不能序列化”的意思是序列化資訊中不包含這個靜態成員域,下面之所以i輸出還是2,是因為測試都在同一個機器(而且是同一個程序),因為這個jvm已經把i載入進來了,所以獲取的是載入好的i,如果是傳到另一臺機器或者關掉程式重新寫個程式讀入DataObject.txt,此時因為別的機器或新的程序是重新載入i的,所以i資訊就是初始時的資訊,即0。所以,總結來看,靜態成員是不能被序列化的,靜態成員定以後的預設初始值是0,所以正確的執行結果應該是:
- word = "123", i = 2
- word = "123", i = 0
那麼既然如此,怎樣才能測試出正確的結果呢?大家注意,上面的程式是直接在一個JVM一個程序中操作完了序列化和反序列化的所有過程,故而JVM中已經儲存了i = 2,所以i的值沒有變化,所以再次讀出來肯定還是2。如果想得出正確的結果,必須在兩個JVM中去測試,但是大家的電腦很難做到這種測試環境,所以可以通過以下方法來測試。
- package test2;
- import java.io.File;
- import java.io.FileNotFoundException;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.ObjectOutputStream;
- /**
- * Description: 測試物件的序列化
- */
- publicclass SerializeDataobject {
- publicstaticvoid main(String[] args) throws Exception {
- // 序列化DataObject物件
- Serialize();
- }
- /**
- * MethodName: SerializePerson
- * Description: 序列化Person物件
- * @author
- * @throws FileNotFoundException
- * @throws IOException
- */
- privatestaticvoid Serialize() throws FileNotFoundException, IOException {
- DataObject object = new DataObject();
- object.setWord("123");
- object.setI(2);
- // 建立ObjectOutputStream物件輸出流,其中用到了檔案的描述符物件和檔案輸出流物件
- ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(
- new File("DataObject.txt")));
- // 將DataObject物件儲存到DataObject.txt檔案中,完成對DataObject物件的序列化操作
- oo.writeObject(object);
- System.out.println("Person物件序列化成功!");
- // 最後一定記得關閉物件描述符!!!
- oo.close();
- }
- }
上面這個類只用來進行序列化,物件被序列化後儲存在檔案"DataObject.txt"中,然後程式執行結束,JVM退出。接下來看另一段程式。
- package test2;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
- import java.io.ObjectInputStream;
- /**
- * Description: 測試物件的反序列
- */
- publicclass DeserializeDataobject {
- publicstaticvoid main(String[] args) throws Exception {
- // 反序列DataObject物件
- DataObject object = Deserialize();
- // 靜態成員屬於類級別的,所以不能序列化,序列化只是序列化了物件而已,
- // 這裡的不能序列化的意思,是序列化資訊中不包含這個靜態成員域,下面
- // 之所以i輸出還是2,是因為測試都在同一個機器(而且是同一個程序),因為這個jvm
- // 已經把i載入進來了,所以獲取的是載入好的i,如果是傳到另一臺機器或者關掉程式重新
- // 寫個程式讀入DataObject.txt,此時因為別的機器或新的程序是重新載入i的,所以i資訊就是初始時的資訊,即0
- System.out.println(object);
- }
- /**
- * MethodName: DeserializePerson
- * Description: 反序列DataObject物件
- * @author
- * @return
- * @throws Exception
- * @throws IOException
- */
- privatestatic DataObject Deserialize() throws Exception, IOException {
- // 建立ObjectInputStream物件輸入流,其中用到了檔案的描述符物件和檔案輸入流物件
- ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
- new File("DataObject.txt")));
- // 從DataObject.txt檔案中讀取DataObject物件,完成對DataObject物件的反序列化操作
- DataObject object = (DataObject) ois.readObject();
- System.out.println("Person物件反序列化成功!");
- // 最後一定記得關閉物件描述符!!!
- ois.close();
- return object;
- }
- }
上面這段程式用來實現物件的反序列化,它從檔案"DataObject.txt"中讀出物件的相關資訊,然後進行了反序列化,最終輸出物件中word和i的值,這個程式輸出的結果才是word = "123", i = 0 這個才是正確的結果,這是因為序列化和反序列化都有自己的main方法,先序列化,然後JVM退出,再次執行反序列化,JVM重新載入DataObject類,此時i = 0,"DataObject.txt"檔案中其實是沒有i的資訊的,只有word的資訊。這裡通過先後執行序列化和反序列化,讓JVM得到一次重新載入類的機會,模擬了兩個JVM下執行的結果。 總之,大家要記住以下幾點: (1)序列化和反序列化的實現方法和應用場合; (2)靜態成員是不能被序列化的,因為靜態成員是隨著類的載入而載入的,與類共存亡,並且靜態成員的預設初始值都是0; (3)要明白錯誤的那個測試程式的原因,搞明白JVM的一些基本機制; (4)要想直接通過列印物件而輸出物件的一些屬性資訊,要重寫toString方法。 上面只是我的一些個人總結,歡迎大家指正和補充。