你好!今天的讲座我们将讨论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Францвоюет 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
来与我们的类一起使用。 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