CodeGym /Java 博客 /随机的 /Java中的序列化和反序列化
John Squirrels
第 41 级
San Francisco

Java中的序列化和反序列化

已在 随机的 群组中发布
你好!在今天的课程中,我们将讨论 Java 中的序列化和反序列化。我们将从一个简单的例子开始。假设您是一名电脑游戏开发人员。如果你在 90 年代长大并记得那个时代的游戏机,你可能知道他们缺少我们今天认为理所当然的东西——保存和加载游戏的能力 :) 如果没有,想象一下!Java 中的序列化和反序列化 - 1恐怕今天没有这些能力的游戏就完蛋了!无论如何,“保存”和“加载”游戏是什么意思?好吧,我们理解日常含义:我们想从我们离开的地方继续游戏。为此,我们创建了一个稍后用于加载游戏的特定“检查点”。但这对程序员而不是休闲游戏玩家意味着什么?答案很简单:我们保存程序的状态。假设您在一款战略游戏中玩西班牙。您的游戏具有状态:每个人拥有哪些领土,每个人拥有多少资源,存在哪些联盟以及与谁结盟,谁处于战争状态,等等。这些信息,即我们程序的状态,必须以某种方式保存,以便恢复数据并继续游戏。当它发生的时候, 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 个流:FileOutputStreamObjectOutputStream。第一个知道如何将数据写入文件,第二个知道如何将对象转换为字节。您已经在之前的课程中看到过类似的嵌套结构,例如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 当然有一些内置的机制。但是,如果我们的可序列化类的字段不是原始字段,而是对其他对象的引用怎么办?例如,让我们创建单独的TerritoryInfoResourceInfoDiplomacyInfo类来与我们的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 中的序列化和反序列化 - 2在 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”一书中阅读关于这个主题的精彩章节,注意它:)
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION