你好!今天的講座我們將討論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 個線程 -FileOutputStream
和ObjectOutputStream
。第一個可以將資料寫入文件,第二個可以將物件轉換為位元組。例如,您已經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
來與我們的類別一起使用。 ResourcesInfo
DiplomacyInfo
SavedGame
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 中使用關鍵字 來解決。如果將此關鍵字新增至類別欄位中,則該欄位的值將不會被序列化。讓我們嘗試創建我們的類別的字段之一,然後我們將序列化並恢復一個物件。 SavedGame
SavedGame
transient
SavedGame 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》一書中有一章介紹這個主題,關註一下:)
GO TO FULL VERSION