JavaRush /Java Blog /Random-TW /Java 中的序列化與反序列化

Java 中的序列化與反序列化

在 Random-TW 群組發布
你好!今天的講座我們將討論Java中的序列化和反序列化。讓我們從一個簡單的例子開始。假設您是一款電腦遊戲的創建者。如果你在 90 年代長大並記得那個時代的遊戲機,你可能知道它們今天缺少一些明顯的東西 - 保存和加載遊戲:) 如果沒有......想像一下!恐怕今天一場比賽如果沒有這樣的機會,就注定失敗!實際上,什麼是“保存”和“加載”遊戲?好吧,從通常的意義上來說,我們明白這是什麼:我們想從上次中斷的地方繼續遊戲。為此,我們創建一種“檢查點”,然後用它來載入遊戲。但這不是日常意義上的,而是「程式設計師」意義上的,這意味著什麼?答案很簡單:我們保存程式的狀態。假設您正在玩一款西班牙策略遊戲。你的遊戲有一個狀態:誰擁有哪些領土,誰擁有多少資源,誰與誰結盟,以及相反,誰處於戰爭狀態,等等。這些訊息,即我們程式的狀態,必須以某種方式保存,以便以後恢復資料並繼續遊戲。這正是序列化反序列化機制的用途。 序列化是將物件的狀態儲存到位元組序列中的過程。 反序列化是從這些位元組重建物件的過程。任何 Java 物件都會轉換為位元組序列。它是做什麼用的?我們不只一次說過,程序本身並不存在。大多數情況下,它們彼此互動、交換資料等。而字節格式對此來說是方便且有效率的。例如,我們可以將類別物件SavedGame(已儲存的遊戲)轉換為位元組序列,透過網路將這些位元組傳輸到另一台計算機,然後在第二台計算機上將這些位元組轉換回 Java 物件!很難聽,對吧?顯然,組織這個過程並不容易:/幸運的是,不!:) 在Java中, Serialized介面負責序列化過程。這個介面非常簡單:您不需要實作單一方法即可使用它!這就是我們的保存遊戲類的樣子:
import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private static final long serialVersionUID = 1L;

   private String[] territoriesInfo;
   private String[] resourcesInfo;
   private String[] diplomacyInfo;

   public SavedGame(String[] territoriesInfo, String[] resourcesInfo, String[] diplomacyInfo){
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public String[] getTerritoriesInfo() {
       return territoriesInfo;
   }

   public void setTerritoriesInfo(String[] territoriesInfo) {
       this.territoriesInfo = territoriesInfo;
   }

   public String[] getResourcesInfo() {
       return resourcesInfo;
   }

   public void setResourcesInfo(String[] resourcesInfo) {
       this.resourcesInfo = resourcesInfo;
   }

   public String[] getDiplomacyInfo() {
       return diplomacyInfo;
   }

   public void setDiplomacyInfo(String[] diplomacyInfo) {
       this.diplomacyInfo = diplomacyInfo;
   }

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoriesInfo=" + Arrays.toString(territoriesInfo) +
               ", resourcesInfo=" + Arrays.toString(resourcesInfo) +
               ", diplomacyInfo=" + Arrays.toString(diplomacyInfo) +
               '}';
   }
}
三個資料集負責領土、經濟和外交等訊息,Serialized介面告訴Java機器:“一切都好,如果有的話,這個類別的物件可以被序列化。” 沒有任何方法的介面看起來很奇怪:/為什麼需要它?這個問題的答案如上:只提供Java機器必要的資訊。在之前的一講中,我們簡單提到了標記介面。這些是特殊的資訊接口,它們只是用將來對 Java 機器有用的附加資訊來標記我們的類別。他們沒有任何需要實施的方法。所以,Serialized 就是這樣的介面之一。private static final long serialVersionUID另一個重要的點:我們在類別中定義的變數。為什麼需要它?此欄位包含序列化類別的唯一版本識別碼。任何實作 Serialized 介面的類別都有一個版本標識符。它是根據類別的內容(欄位、聲明順序、方法)計算的。如果我們更改類別中的欄位類型和/或欄位數量,版本識別碼將立即變更。在類別序列化時也會寫入serialVersionUID。當我們嘗試反序列化(即從一組位元組恢復物件)時,會將該值與程式中類別的serialVersionUID值進行比較。serialVersionUID如果值不匹配,則會拋出 java.io.InvalidClassException。我們將在下面看到一個例子。為了避免這種情況,我們只需為我們的類別手動設定此版本 ID。在我們的例子中,它只是等於 1(您可以替換為您喜歡的任何其他數字)。好吧,是時候嘗試序列化我們的對象SavedGame,看看會發生什麼!
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // create our object
       String[] territoryInfo = {"Spain has 6 provinces", "Russia has 10 provinces", "France has 8 provinces"};
       String[] resourcesInfo = {"Spain has 100 gold", "Russia has 80 gold", "France has 90 gold"};
       String[] diplomacyInfo = {"France is at war with Russia, Spain has taken a position of neutrality"};

       SavedGame savedGame = new SavedGame(territoryInfo, resourcesInfo, diplomacyInfo);

       //create 2 threads to serialize the object and save it to a file
       FileOutputStream outputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

       // save game to file
       objectOutputStream.writeObject(savedGame);

       // close the stream and release resources
       objectOutputStream.close();
   }
}
正如您所看到的,我們創建了 2 個線程 -FileOutputStreamObjectOutputStream。第一個可以將資料寫入文件,第二個可以將物件轉換為位元組。例如,您已經new BufferedReader(new InputStreamReader(...))在之前的講座中看到過類似的“嵌套”結構,因此它們不應該嚇到您:)通過創建這樣的兩個線程“鏈”,我們執行這兩項任務:我們將對象轉換SavedGame為一組位元組並使用方法將其保存到文件中writeObject()。順便說一句,我們甚至沒有檢查我們得到了什麼!是時候看一下文件了! *注意:該文件不需要事先建立。如果該名稱的檔案不存在,它將自動建立* 以下是其內容! Øн sr SavedGame [ DiplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ TerritoriesInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранѕ №, РоС ЃРїР° e n s usus。 РѕСЃСЃРёРё 80 золотаt !РЈ ФранС...... ании 6 РїСЂРѕРІ ёРЅС †РёРё %РЈ Р РѕСЃКЃР РёРут &РЈ ФранцРеРё 8 проввинциРуввинциРуРІРІРёРЅС† РёРуІРР只是一個物件或文字?好吧,這就是這個集合的樣子:)這是我們保存的遊戲!如果我們想恢復原來的對象,即從我們停止的地方加載並繼續遊戲,我們需要逆向過程、反序列化...這就是我們例子中的樣子:
import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

       SavedGame savedGame = (SavedGame) objectInputStream.readObject();

       System.out.println(savedGame);
   }
}
這就是結果! SavedGame{territoriesInfo=[西班牙有6個省,俄羅斯有10個省,法國有8個省], resourcesInfo=[西班牙有100金,俄羅斯有80金,法國有90金], DiplomacyInfo=[法國與俄羅斯交戰,西班牙已佔據中立地位]} 太棒了!我們設法首先將遊戲的狀態保存到文件中,然後從文件中恢復它。現在讓我們嘗試做同樣的事情,但從SavedGame我們的類別中刪除版本標識符。我們不會重寫這兩個類,它們中的程式碼將是相同的,我們只需SavedGame刪除private static final long serialVersionUID. 這是序列化後的物件: Øн sr SavedGameі€MіuОm‰ [ DiplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ TerritoriesInfoq ~ xpur [Ljava.lang.String;ТВзй{ ‚ СЃ Россией, Р∼спанЏзанЏРЂР°Р»РёС‚етаuq ~ t "РЈ Р約СРЃРї °РЅРёРё 100 Р · олотаt РЈ Р РѕСЃСЃРёРё 80 золотаЂаuq ~ t &РЈ Р∼спан ёРё 6 R їСЂРѕРІРёРЅС† РёР№t %РЈ Р РѕСЃСЃРёРё 10 РїСЂРѕРІРЅРёРЅР ЂРѕРІ ІРёРЅС†РёРЅА 當嘗試反序列化它時,發生了這樣的情況: InvalidClassException : local class incomplete : stream classdesc serialVersionUID = - local class incomplete : stream classdesc serialVersionUID = - local class incomplete : stream 14045 ,本地類serialVersionUID = -6675950253085108747 這與上面提到的異常相同。您可以在我們一位學生的文章中閱讀更多相關內容。順便說一句,我們錯過了一個重要的點。很明顯,字符串和基元很容易序列化:Java 當然有一些,然後有內建的機制。但是,如果我們的serializable類別具有不表示為基元而是表示為對其他物件的引用的欄位怎麼辦?例如,讓我們建立單獨的類別TerritoriesInfo來與我們的類別一起使用。 ResourcesInfoDiplomacyInfoSavedGame
public class TerritoriesInfo {

   private String info;

   public TerritoriesInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "TerritoriesInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}

public class ResourcesInfo {

   private String info;

   public ResourcesInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "ResourcesInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}

public class DiplomacyInfo {

   private String info;

   public DiplomacyInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "DiplomacyInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}
但現在我們有一個問題:如果我們想序列化更改後的類,所有這些類都應該是可序列化的嗎SavedGame
import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private TerritoriesInfo territoriesInfo;
   private ResourcesInfo resourcesInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public TerritoriesInfo getTerritoriesInfo() {
       return territoriesInfo;
   }

   public void setTerritoriesInfo(TerritoriesInfo territoriesInfo) {
       this.territoriesInfo = territoriesInfo;
   }

   public ResourcesInfo getResourcesInfo() {
       return resourcesInfo;
   }

   public void setResourcesInfo(ResourcesInfo resourcesInfo) {
       this.resourcesInfo = resourcesInfo;
   }

   public DiplomacyInfo getDiplomacyInfo() {
       return diplomacyInfo;
   }

   public void setDiplomacyInfo(DiplomacyInfo diplomacyInfo) {
       this.diplomacyInfo = diplomacyInfo;
   }

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoriesInfo=" + territoriesInfo +
               ", resourcesInfo=" + resourcesInfo +
               ", diplomacyInfo=" + diplomacyInfo +
               '}';
   }
}
好吧,讓我們在實踐中檢查一下!讓我們暫時保留所有內容並嘗試序列化物件SavedGame
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // create our object
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourcesInfo resourcesInfo = new ResourcesInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("France is at war with Russia, Spain has taken a position of neutrality");


       SavedGame savedGame = new SavedGame(territoriesInfo, resourcesInfo, diplomacyInfo);

       FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

       objectOutputStream.writeObject(savedGame);

       objectOutputStream.close();
   }
}
結果: 在線程“main”中出現異常 java.io.NotSerializedException:DiplomacyInfo 失敗!事實上,這就是我們問題的答案。當您序列化一個物件時,它在其實例變數中引用的所有物件都會被序列化。如果這些物件也引用第三個對象,它們也會被序列化。如此下去,無止境。該鏈中的所有類別都必須是可序列化的,否則它們將不可序列化並會拋出異常。順便說一句,這可能會在將來產生問題。例如,如果我們在序列化過程中不需要類別的一部分,我們該怎麼做?或者,例如,TerritoryInfo我們在程式中繼承了一個類別作為某個庫的一部分。但是,它不是可序列化的,因此我們無法更改它。事實證明,我們不能TerritoryInfo向我們的類別添加字段,因為那樣整個類別將變得不可序列化!問題:/ 此類問題在 Java 中使用關鍵字 來解決。如果將此關鍵字新增至類別欄位中,則該欄位的值將不會被序列化。讓我們嘗試創建我們的類別的字段之一,然後我們將序列化並恢復一個物件。 SavedGameSavedGameJava 中的序列化與反序列化 - 2transientSavedGame transient
import java.io.Serializable;

public class SavedGame implements Serializable {

   private transient TerritoriesInfo territoriesInfo;
   private ResourcesInfo resourcesInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   //...getters, setters, toString()...
}



import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // create our object
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourcesInfo resourcesInfo = new ResourcesInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("France is at war with Russia, Spain has taken a position of neutrality");


       SavedGame savedGame = new SavedGame(territoriesInfo, resourcesInfo, diplomacyInfo);

       FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

       objectOutputStream.writeObject(savedGame);

       objectOutputStream.close();
   }
}


import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

       SavedGame savedGame = (SavedGame) objectInputStream.readObject();

       System.out.println(savedGame);

       objectInputStream.close();


   }
}
結果如下: SavedGame{territoriesInfo=null, resourcesInfo=ResourcesInfo{info='西班牙有 100 金幣,俄羅斯有 80 金幣,法國有 90 金幣'}, DiplomacyInfo=DipromacyInfo{info='法國與俄羅斯交戰,西班牙採取了中立立場'}} 同時,我們收到了關於將給transient- 字段賦予什麼值的問題的答案。它被分配了一個預設值。對於對象來說,這是null。閒暇時,您可以閱讀這篇關於序列化的優秀文章。它還談到了介面Externalizable,我們將在下一講中討論。另外,《Head-First Java》一書中有一章介紹這個主題,關註一下:)
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION