你好!在今天的課程中,我們將討論 Java 中的序列化和反序列化。我們將從一個簡單的例子開始。假設您是一名電腦遊戲開發人員。如果你在 90 年代長大並記得那個時代的遊戲機,你可能知道他們缺少我們今天認為理所當然的東西——保存和加載遊戲的能力 :) 如果沒有,想像一下!恐怕今天沒有這些能力的遊戲就完蛋了!無論如何,“保存”和“加載”遊戲是什麼意思?好吧,我們理解日常含義:我們想從我們離開的地方繼續遊戲。為此,我們創建了一個稍後用於加載遊戲的特定“檢查點”。但這對程序員而不是休閒遊戲玩家意味著什麼?答案很簡單:我們保存程序的狀態。假設您在一款戰略遊戲中玩西班牙。您的遊戲具有狀態:每個人擁有哪些領土,每個人擁有多少資源,存在哪些聯盟以及與誰結盟,誰處於戰爭狀態,等等。這些信息,即我們程序的狀態,必須以某種方式保存,以便恢復數據並繼續遊戲。當它發生的時候, Java 中的序列化是將對象的狀態保存為字節序列的過程。 Java 中的反序列化是從這些字節中恢復對象的過程。任何 Java 對像都可以轉換為字節序列。我們為什麼需要這個?我們一再說過,程序本身並不存在。大多數情況下,它們會相互交互、交換數據等。字節格式對此既方便又高效。例如,我們可以轉換SavedGame的一個對象class 轉換成字節序列,通過網絡將這些字節傳輸到另一台計算機,然後在另一台計算機上將這些字節轉換回 Java 對象!聽起來很難吧?似乎很難讓這一切發生:/幸運的是,事實並非如此!:) 在Java中,Serializable接口負責序列化過程。這個接口非常簡單:您不必實現一個方法就可以使用它!看看我們保存遊戲的類是多麼簡單:
import java.io.Serializable;
import java.util.Arrays;
public class SavedGame implements Serializable {
private static final long serialVersionUID = 1L;
private String[] territoryInfo;
private String[] resourceInfo;
private String[] diplomacyInfo;
public SavedGame(String[] territoryInfo, String[] resourceInfo, String[] diplomacyInfo){
this.territoryInfo = territoryInfo;
this.resourceInfo = resourceInfo;
this.diplomacyInfo = diplomacyInfo;
}
public String[] getTerritoryInfo() {
return territoryInfo;
}
public void setTerritoryInfo(String[] territoryInfo) {
this.territoryInfo = territoryInfo;
}
public String[] getResourceInfo() {
return resourceInfo;
}
public void setResourceInfo(String[] resourceInfo) {
this.resourceInfo = resourceInfo;
}
public String[] getDiplomacyInfo() {
return diplomacyInfo;
}
public void setDiplomacyInfo(String[] diplomacyInfo) {
this.diplomacyInfo = diplomacyInfo;
}
@Override
public String toString() {
return "SavedGame{" +
"territoryInfo=" + Arrays.toString(territoryInfo) +
", resourceInfo=" + Arrays.toString(resourceInfo) +
", diplomacyInfo=" + Arrays.toString(diplomacyInfo) +
'}';
}
}
三個數組負責有關領土、資源和外交的信息,而 Serializable 接口告訴 Java 機器:“如果這個類的對象可以被序列化,一切都很好”。沒有單一接口的接口看起來很奇怪:/為什麼有必要?上面給出了該問題的答案:只需要向 Java 機器提供必要的信息。在過去的課程中,我們簡要提到了標記接口。這些是特殊的信息接口,它們簡單地用將來對 Java 機器有用的附加信息標記我們的類。他們沒有任何您必須實施的方法。這是Serializable——一個這樣的接口。這是另一個重點:為什麼我們需要我們在類中定義的private static final long serialVersionUID變量?該字段包含序列化類的唯一版本標識符。每個實現Serializable接口的類都有一個版本標識符。它是根據類的內容來確定的——字段及其聲明順序,方法及其聲明順序。如果我們更改類中的字段類型和/或字段數量,版本標識符會立即更改。serialVersionUID也是在類序列化的時候寫的。當我們嘗試反序列化時,即從字節序列中恢復對象時,serialVersionUID的值與serialVersionUID的值進行比較我們課程中的課程。如果值不匹配,則會拋出 java.io.InvalidClassException。我們將在下面看到一個例子。為了避免這種情況,我們只需手動為我們的類設置版本標識符。在我們的例子中,它將簡單地等於 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[] resourceInfo = {"Spain has 100 gold", "Russia has 80 gold", "France has 90 gold"};
String[] diplomacyInfo = {"France is at war with Russia, Spain has taken a neutral position"};
SavedGame savedGame = new SavedGame(territoryInfo, resourceInfo, diplomacyInfo);
// Create 2 streams 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 the game to a file
objectOutputStream.writeObject(savedGame);
// Close the stream and release resources
objectOutputStream.close();
}
}
如您所見,我們創建了 2 個流:FileOutputStream和ObjectOutputStream。第一個知道如何將數據寫入文件,第二個知道如何將對象轉換為字節。您已經在之前的課程中看到過類似的嵌套結構,例如new BufferedReader(new InputStreamReader(...)),所以它們不應該嚇到您 :) 通過創建這個包含兩個流的鏈,我們執行了兩個任務:我們將SavedGame對象轉換為字節序列,並使用writeObject()方法將其保存到文件中。而且,順便說一句,我們甚至沒有看我們得到了什麼!是時候查看文件了! *注意:不需要提前創建文件。如果具有指定名稱的文件不存在,則會自動創建它* 以下是其內容: ¬н sr SavedGame [ diplomacyInfo [Ljava/lang/String;[ resourceInfoq ~ [ territoryInfoq ~ xpur [Ljava.lang. String;¬ТVзй{G xp t pФраРIVция воюет СЃ Россией, Р成為спани ¡ РµС,Р°uq ~ t "РЈ Р∼спаЁии 100 золоС,Р°t РЈ Р РѕССЃРёРё 80 Р·РѕР»РѕС ‚Р°t !РЈ Франции 90 Р·РѕР »РѕС,Р°uq ~ t &РЈ Р∼спании 6 провинцийt %РЈ РРѕСЃСРёРё 10 провинциРt &РЈ Франции 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{territoryInfo=[西班牙有6個省份,俄羅斯有10個省份,法國有8個省份],resourceInfo=[西班牙有100金幣,俄羅斯有80金幣,法國有90金幣],diplomacyInfo=[法國與俄羅斯交戰,西班牙採取了中立立場]} 太好了!我們設法首先將游戲狀態保存到文件中,然後從文件中恢復它。現在讓我們嘗試做同樣的事情,但我們將從SavedGame類中刪除版本標識符。我們不會重寫我們的兩個類。他們的代碼將是相同的。我們將從SavedGame類中刪除private static final long serialVersionUID。這是序列化後的對象: ¬н sr SavedGameі€MіuОm‰ [ diplomacyInfo [Ljava/lang/String;[ resourceInfoq ~ [ territoryInfoq ~ xpur [Ljava.lang.String;¬ТVзй{G xp t pФранция РІРѕСЋР µС‚ СЃ Россией, Р∼спания заняла позициц нейС,ралиС,РµС,Р°uq ~ t "РЈ Р∼спании 100 золотР°t РЈ Р РѕСЃСЃРёРё 80 золоС,Р°t !РЈ Франции 90 золоС,Р°uq ~ t & РЈ Р∼спании 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10 провивинцийt &РЈ Франции 8 РїСЂРѕРІР ёРЅС†РёР№ 但是看看當我們嘗試反序列化時會發生什麼: InvalidClassException: local class incompatible: stream classdesc serialVersionUID = -196410440475012755, local class serialVersionUID = -6675950253085108747 順便說一句,我們錯過了一些重要的事情。顯然,字符串和原語很容易序列化:Java 當然有一些內置的機制。但是,如果我們的可序列化類的字段不是原始字段,而是對其他對象的引用怎麼辦?例如,讓我們創建單獨的TerritoryInfo、ResourceInfo和DiplomacyInfo類來與我們的SavedGame類一起工作。
public class TerritoryInfo {
private String info;
public TerritoryInfo(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "TerritoryInfo{" +
"info='" + info + '\'' +
'}';
}
}
public class ResourceInfo {
private String info;
public ResourceInfo(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "ResourceInfo{" +
"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 TerritoryInfo territoryInfo;
private ResourceInfo resourceInfo;
private DiplomacyInfo diplomacyInfo;
public SavedGame(TerritoryInfo territoryInfo, ResourceInfo resourceInfo, DiplomacyInfo diplomacyInfo) {
this.territoryInfo = territoryInfo;
this.resourceInfo = resourceInfo;
this.diplomacyInfo = diplomacyInfo;
}
public TerritoryInfo getTerritoryInfo() {
return territoryInfo;
}
public void setTerritoryInfo(TerritoryInfo territoryInfo) {
this.territoryInfo = territoryInfo;
}
public ResourceInfo getResourceInfo() {
return resourceInfo;
}
public void setResourceInfo(ResourceInfo resourceInfo) {
this.resourceInfo = resourceInfo;
}
public DiplomacyInfo getDiplomacyInfo() {
return diplomacyInfo;
}
public void setDiplomacyInfo(DiplomacyInfo diplomacyInfo) {
this.diplomacyInfo = diplomacyInfo;
}
@Override
public String toString() {
return "SavedGame{" +
"territoryInfo=" + territoryInfo +
", resourceInfo=" + resourceInfo +
", 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
TerritoryInfo territoryInfo = new TerritoryInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
ResourceInfo resourceInfo = new ResourceInfo("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 neutral position");
SavedGame savedGame = new SavedGame(territoryInfo, resourceInfo, diplomacyInfo);
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(savedGame);
objectOutputStream.close();
}
}
結果: 線程“main”中的異常 java.io.NotSerializableException:DiplomacyInfo 它沒有用!所以,這是我們問題的答案。當一個對像被序列化時,它的實例變量引用的所有對像都被序列化。如果這些對像還引用了其他對象,那麼它們也會被序列化。永遠持續下去。此鏈中的所有類都必須是Serializable,否則將無法序列化它們並拋出異常。順便說一句,這可能會在未來產生問題。比如我們在序列化的時候不需要某個類的一部分怎麼辦?或者,如果我們“通過繼承”將TerritoryInfo類作為庫的一部分會怎麼樣?進一步假設它不是因此,我們無法更改它。這意味著我們不能將TerritoryInfo字段添加到我們的SavedGame類中,因為那樣的話整個SavedGame類將變得不可序列化!那是個問題: / 在 Java 中,這類問題是通過transient關鍵字解決的。如果將此關鍵字添加到類的某個字段,則該字段不會被序列化。讓我們嘗試使我們的SavedGame類的字段之一成為瞬態,然後我們將序列化並恢復單個對象。
import java.io.Serializable;
public class SavedGame implements Serializable {
private transient TerritoryInfo territoryInfo;
private ResourceInfo resourceInfo;
private DiplomacyInfo diplomacyInfo;
public SavedGame(TerritoryInfo territoryInfo, ResourceInfo resourceInfo, DiplomacyInfo diplomacyInfo) {
this.territoryInfo = territoryInfo;
this.resourceInfo = resourceInfo;
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
TerritoryInfo territoryInfo = new TerritoryInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
ResourceInfo resourceInfo = new ResourceInfo("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 neutral position");
SavedGame savedGame = new SavedGame(territoryInfo, resourceInfo, 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{territoryInfo=null, resourceInfo=ResourceInfo{info='西班牙有 100 金幣,俄羅斯有 80 金幣,法國有 90 金幣'}, diplomacyInfo=DiplomacyInfo{info='法國與俄羅斯、西班牙交戰已經採取了中立的立場'}}也就是說,我們得到了將什麼值分配給 瞬態字段的問題的答案。它被分配了默認值。對於對象,這是null。你可以在“Head-First Java”一書中閱讀關於這個主題的精彩章節,注意它:)
GO TO FULL VERSION