JavaRush /Java 博客 /Random-ZH /Java 中的序列化和反序列化

Java 中的序列化和反序列化

已在 Random-ZH 群组中发布
你好!今天的讲座我们将讨论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Францвоюет SЃ R оссией, РоС ЃРїР° e n s usus。 РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Р∼спании 6 РїСЂРѕРІ ёРЅС †РёРё %РЈ Р РѕСЃСЃРёРё 10 РїСЂРѕРІРёРЅС †РёРут &РЈ ФранцРеРё 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;ТВзй{G xp t pФранция РІРѕСЋРµС ‚ СЃ Россией, Р∼спанЏзаняла RїРѕР·РёС†РёСЋ нейтралитетаuq ~ t "РЈ Р约СРЃРї °РЅРёРё 100 Р · олотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ ФранСРёРё 90 золотаuq ~ t &РЈ Р∼спан ёРё 6 R їСЂРѕРІРёРЅС† РёР№t %РЈ Р РѕСЃСЃРёРё 10 провнинциРут &РЈ Франции 8 РїСЂРѕРІ ІРёРЅС†РёРЅА 当尝试反序列化它时,发生了这样的情况: InvalidClassException: local class incomplete : stream classdesc serialVersionUID = - 196410440475012755,本地类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