CodeGym /Java 博客 /随机的 /Java中的序列化和反序列化有什么区别?
John Squirrels
第 41 级
San Francisco

Java中的序列化和反序列化有什么区别?

已在 随机的 群组中发布
你好!在今天的课程中,我们将讨论 Java 中的序列化和反序列化。我们将从一个简单的例子开始。假设您创建了一款电脑游戏。如果你在 90 年代长大并记得那个时代的游戏机,你可能知道他们缺少我们今天认为理所当然的东西——保存和加载游戏的能力 :) 如果没有,想象一下! Java中的序列化和反序列化有什么区别? - 1 恐怕没有这些能力的游戏今天就完蛋了!“保存”和“加载”游戏到底是什么意思?好吧,我们理解普通的意思:我们想从我们离开的地方继续游戏。为此,我们创建了一种“检查点”,然后我们用它来加载游戏。但这对程序员而不是休闲游戏玩家意味着什么?答案很简单:我们'. 假设您在 Strategium 中扮演西班牙。你的游戏有一个状态:谁拥有哪些领土,谁拥有多少资源,谁与谁结盟,谁与谁交战,等等。我们必须以某种方式保存这些信息,即我们程序的状态,以便将来恢复它并继续游戏。因为这正是序列化去序列化的目的。 序列化是将对象的状态存储在字节序列中的过程。 反序列化是从这些字节中恢复对象的过程。任何 Java 对象都可以转换为字节序列。我们为什么需要那个?我们不止一次说过,程序本身并不存在。大多数情况下,它们与其他程序交互、交换数据等。字节序列是一种方便高效的格式。例如,我们可以将我们的SavedGame对象变成字节序列,通过网络将这些字节发送到另一台计算机,然后在第二台计算机上将这些字节转回 Java 对象!听起来很难,对吧?实施这个过程似乎很痛苦:/幸运的是,事实并非如此!:) 在 Java 中,Serializable接口负责序列化过程。这个接口非常简单:你不需要实现一个方法就可以使用它!这就是我们的游戏保存类看起来多么简单:

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) +
               '}';
   }
}
三阵分别负责领土、资源、外交等信息。Serializable 接口告诉 Java 虚拟机:“一切正常——如有必要,可以序列化此类的对象”。没有单一接口的接口看起来很奇怪:/为什么有必要?这个问题的答案见上文:它只是起到提供给Java虚拟机必要的信息的作用。在我们之前的一节课中,我们简要提到了标记接口。这些是特殊的信息接口,它们简单地用将来对 Java 机器有用的附加信息标记我们的类。他们没有任何您必须实施的方法。Serializable是这些接口之一。另一个要点:为什么我们需要private static final long serialVersionUID在类中定义的变量?为什么需要它?该字段包含序列化类版本的唯一标识符。任何实现该Serializable接口的类都有一个version标识符。它是根据类的内容计算的:它的字段、它们声明的顺序、方法等。如果我们更改字段类型和/或类中字段的数量,那么版本标识符会立即更改. 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[] 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 neutral position"};

       SavedGame savedGame = new SavedGame(territoryInfo, resourcesInfo, 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 free 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Франция воюет СЃ Россией, Испания заняла позицию нейтралитета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{territoriesInfo=["Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces], resourcesInfo=[Spain has 100 gold, Russia has 80 gold, France has 90 gold], diplomacyInfo=[France is at war with Russia, Spain has taken a neutral position]}
出色的!我们设法先将游戏状态保存到文件中,然后从文件中恢复它。现在让我们尝试做同样的事情,但是没有我们SavedGame类的版本标识符。我们不会重写我们的两个类。他们的代码将保持不变,但我们将从类private static final long serialVersionUID中删除SavedGame这是序列化后的对象:

¬н sr SavedGameі€MіuОm‰ [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ 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 可能有某种内置机制可以做到这一点。但是,如果我们的serializable类的字段不是原始字段,而是对其他对象的引用怎么办?例如,让我们创建单独的TerritoriesInfo,ResourcesInfoDiplomacyInfo类来处理我们的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 + '\'' +
               '}';
   }
}
现在出现了一个问题:如果我们想要序列化我们改变的类,是否需要所有这些类?SerializableSavedGame

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
       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(territoriesInfo, resourcesInfo, diplomacyInfo);

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

       objectOutputStream.writeObject(savedGame);

       objectOutputStream.close();
   }
}
结果:

Exception in thread "main" java.io.NotSerializableException: DiplomacyInfo
没用!基本上,这就是我们问题的答案。当一个对象被序列化时,它的实例变量引用的所有对象都被序列化。 如果这些对象还引用了其他对象,那么它们也会被序列化。依此类推。这个链中的所有类都必须是Serializable,否则无法序列化,会抛出异常。顺便说一句,这可能会在未来产生问题。例如,如果我们在序列化时不需要某个类的一部分,我们应该怎么做?或者,例如,如果该类TerritoryInfo作为某个第三方库的一部分提供给我们怎么办。并进一步假设它不是Serializable,因此我们无法更改它。事实证明,我们无法TerritoryInfo在我们的SavedGame类,因为这样做会使整个SavedGame类不可序列化!That's a problem:/ Java中的序列化和反序列化有什么区别? - 2在 Java 中,这类问题使用transient关键字来解决。如果将此关键字添加到类的某个字段,则该字段不会被序列化。让我们尝试使类的实例字段之一成为SavedGame瞬态的。然后我们将序列化并恢复一个对象。

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
       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(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='Spain has 100 gold, Russia has 80 gold, France has 90 gold'}, diplomacyInfo=DiplomacyInfo{info='France is at war with Russia, Spain has taken a neutral position'}}
此外,我们得到了有关为字段分配什么值的问题的答案transient。它被分配了默认值。对于对象,这是null. 如果您有几分钟空闲时间,可以阅读这篇关于连载的优秀文章。它还提到了Externalizable接口,我们将在下一课中讨论。此外,“Head-First Java”一书有一章是关于这个主题的。给它一些关注:)
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION