Java网络编程——对象的序列化与反序列化

当两个进程进行远程通信时,彼此可以发送各种类型的数据,如文本、图片、语音和视频等。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。当两个Java进程进行远程通信时,一个进程能否把一个Java对象发送给另一个进程呢?答案是肯定的。不过,发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。把Java对象转换为字节序列的过程称为对象的序列化;把字节序列恢复为Java对象的过程称为对象的反序列化。

可以通过一个比喻来帮助我们形象地理解对象的序列化以及反序列化。假定要把一批新汽车(Car对象)从美国海运到中国。为了便于运输,在美国,先把汽车拆成一个个部件,这个过程相当于对象的序列化。汽车部件到达中国后,再把这些部件组装成汽车,这个过程相当于对象的反序列化。

当程序运行时,程序所创建的各种对象都位于内存中,当程序运行结束,这些对象就结束生命周期。如下图所示,对象的序列化主要有两种用途:

  • (1)把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中。
  • (2)在网络上传送对象的字节序列。

在这里插入图片描述

1、JDK类库中的序列化API

java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化成一个对象,并将其返回。

只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则ObjectOutputStream的writeObject(Object obj)方法会抛出IOException。实现Serializable或Externalizable接口的类也被称为可序列化类。Externalizable接口继承自Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式。JDK类库中的部分类(如String类、包装类和Date类等)都实现了Serializable接口。

假定有一个名为Customer的类,它的对象需要序列化。如果Customer类仅仅实现了Serializable接口的类,那么将按照以下方式序列化以及反序列化Customer对象:

  • ObjectOutputStream采用JDK提供的默认的序列化方式,对Customer对象的非transient类型的实例变量进行序列化。
  • ObjectInputStream采用JDK提供的默认的反序列化方式,对Customer对象的非transient类型的实例变量进行反序列化。

如果Customer类不仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOuputStream out)方法,那么将按照以下方式序列化以及反序列化Customer对象:

  • ObjectOutputStream调用Customer类的writeObject(ObjectOuputStream out)方法来进行序列化。
  • ObjectInputStream调用Customer类的readObject(ObjectInputStream in)方法来进行反序列化。

如果Customer类实现了Externalizable接口,那么Customer类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法。在这种情况下,将按照以下方式序列化以及反序列化Customer对象:

  • ObjectOutputStream调用Customer类的writeExternal(ObjectOutput out)方法来进行序列化。
  • ObjectInputStream先通过Customer类的不带参数的构造方法创建一个Customer对象,然后调用它的readExternal(ObjectInput int)方法来进行反序列化。

下图是序列化API的类框图:
在这里插入图片描述

2、 实现Serializable接口

bjectOuputStream只能对实现了Serializable接口的类的对象进行序列化。在默认情况下,ObjectOuputStream按照默认方式序列化,这种序列化方式仅仅对一个对象的非transient类型的实例变量进行序列化,而不会序列化对象的transient(transient是一个修饰符,用于标记一个实例变量不参与序列化过程)类型的实例变量,也不会序列化静态变量。

下面的Customer1类中,定义了一些静态变量、非transient类型的实例变量,以及transient类型的实例变量。

import java.io.Serializable;/*** @title Customer* @description 测试* @author: yangyongbing* @date: 2023/12/8 15:30*/
public class Customer implements Serializable {// 用于计算Customer对象的数目private static int count;private static final int MAX_COUNT=1000;private String name;private transient String password;static {System.out.println("调用Customer类的静态代码块");}public Customer() {System.out.println("调用Customer类的不带参数的构造方法");count++;}public Customer(String name, String password) {System.out.println("调用Customer类的带参数的构造方法");this.name = name;this.password = password;count++;}@Overridepublic String toString() {return "count=" +count+" MAX_COUNT="+MAX_COUNT+" name=" + name+" password=" + password;}public static void main(String[] args) {Customer customer = new Customer();System.out.println(customer);}
}

先运行命令“java SimpleServer Customer”,再运行命令“java SimpleClient”。SimpleServer端的打印结果如下。
在这里插入图片描述
SimpleClient端的打印结果如下:
在这里插入图片描述
从以上打印结果可以看出,当ObjectOutputStream按照默认方式序列化时,Customer对象的静态变量count,以及transient类型的实例变量password没有被序列化。

当ObjectInputStream按照默认方式反序列化时,有以下特点:

  • 如果在内存中对象所属的类还没有被加载,那么会先加载并初始化这个类。如果在classpath中不存在相应的类文件,那么会抛出ClassNotFoundException。从SimpleClient端的打印结果可以看出,客户端加载并初始化了Customer类,在初始化时,把静态常量MAX_COUNT初始化为1000,并且把静态变量count初始化为0,此外还调用了Customer类的静态代码块。
  • 在反序列化时不会调用类的任何构造方法。

如果一个实例变量被transient修饰符修饰,那么默认的序列化方式不会对它序列化。根据这一特点,可以用transient修饰符来修饰以下类型的实例变量。

(1)实例变量不代表对象的固有的内部数据,仅仅代表具有一定逻辑含义的临时数据。例如,假定Customer类有firstName、lastName和fullName等属性:
在这里插入图片描述
Customer类的fullName实例变量可以不必被序列化,因为知道了firstName和lastName变量的值,就可以由它们推导出fullName实例变量的值。

(2)实例变量表示一些比较敏感的信息(比如银行账户的口令),出于安全方面的原因,不希望对其序列化。

(3)实例变量需要按照用户自定义的方式序列化,比如经过加密后再序列化。在这种情况下,可以把实例变量定义为transient类型,然后在writeObject()方法中对其序列化

2.1、序列化对象图

类与类之间可能存在关联关系。例如下列中的Customer2类与Order2类之间存在一对多的双向关联关系。
在这里插入图片描述
SimpleServer类的main()方法中,以下代码创建了一个Customer2对象和两个Order2对象,并且建立了它们的关联关系:
在这里插入图片描述
下图显示了在内存中,以上代码创建的3个对象之间的关联关系:
在这里插入图片描述
当通过ObjectOutputStream对象的writeObject(customer)方法序列化Customer2对象时,会不会序列化与它关联的Order2对象呢?答案是肯定的。在默认方式下,对象输出流会对整个对象图进行序列化。当程序执行writeObject(customer)方法时,该方法不仅序列化Customer2对象,还会把两个与它关联的Order2对象也序列化。当通过ObjectInputStream对象的readObject()方法反序列化Customer2对象,实际上会对整个对象图反序列化。

先运行命令“java SimpleServer Customer2”,再运行命令“java SimpleClient”。SimpleServer服务器会把由一个Customer2对象和两个Order2对象构成的对象图发送给SimpleClient。SimpleClient端的打印结果如下:
在这里插入图片描述

按照默认方式序列化对象A时,到底会序列化由哪些对象构成的对象图呢?如下图所示:
在这里插入图片描述

从对象A到对象B之间的箭头表示从对象A到对象B有关联关系,或者说,对象A持有对象B的引用,或者说,在内存中可以从对象A导航到对象B。序列化对象A时,实际上会序列化对象A,以及所有可以从对象A直接或间接导航到的对象。因此序列化对象A时,实际上在对象图中被序列化的对象包括:对象A、对象B、对象C、对象D、对象E、对象F和对象G。

2.2、控制序列化的行为

如果用户希望控制类的序列化行为,那么可以在可序列化类中提供以下形式的writeObject()方法和readObject()方法:
在这里插入图片描述
当ObjectOutputStream对一个Customer对象进行序列化时,如果该Customer对象具有writeObject()方法,那么就会执行这一方法,否则就按默认方式序列化。在ObjectOutputStream的defaultWriteObject()方法中指定了默认的序列化操作。

在Customer对象的writeObject()方法中,可以先调用ObjectOutputStream的defaultWriteObject()方法,使得对象输出流先执行默认的序列化操作。

当ObjectInputStream对一个Customer对象进行反序列化时,如果该Customer对象具有readObject()方法,那么就会执行这一方法,否则就按默认方式反序列化。在ObjectInputStream的defaultReadObject()方法中指定了默认的反序列化操作。

在Customer对象的readObject()方法中,可以先调用ObjectInputStream的defaultReadObject()方法,使得对象输入流先执行默认的反序列化操作。

值得注意的是,以上writeObject()方法和readObject()方法并不是在java.io.Serializable接口中被定义的。当一个软件系统希望扩展第三方提供的Java类库(比如JDK类库)的功能时,最常见的方式是实现第三方类库的一些接口,或创建类库中抽象类的子类。但是以上writeObject()方法和readObject()方法并不是在java.io.Serializable接口中被定义的。JDK类库的设计人员没有把这两个方法放在Serializable接口中,这样做的优点如下所述:

  • (1)不必公开这两个方法的访问权限,以便封装序列化的细节。如果把这两个方法放在Serializable接口中,就必须定义为public类型。
  • (2)不必强迫用户定义的可序列化类实现这两个方法。如果把这两个方法放在Serializable接口中,它的实现类就必须实现这些方法,否则就只能声明为抽象类。

在以下情况下,可以考虑采用用户自定义的序列化方式,从而控制序列化的行为:

  • (1)确保序列化的安全性,对敏感的信息加密后再序列化,在反序列化时则需要解密。
  • (2)确保对象的成员变量符合正确的约束条件。
  • (3)优化序列化的性能。
  • (4)便于更好地封装类的内部数据结构,确保类的接口不会被类的内部实现所束缚。

2.3、readResolve()方法在单例类中的运用

单例类指仅有一个实例的类。在系统中具有唯一性的组件可作为单例类,这种类的实例通常会占用较多的内存,或者实例的初始化过程比较冗长,因此随意创建这些类的实例会影响系统的性能。

下面的GlobalConfig类就是个单例类,它用来存放软件系统的配置信息。这些配置信息本来存放在配置文件中,在GlobalConfig类的构造方法中,会从配置文件中读取配置信息,把它存放在properties属性中:
在这里插入图片描述
无论是采用默认方式,还是采用用户自定义的方式,反序列化都会创建一个新的对象。在以上GlobalConfig类的main()方法中,in.readObject()方法会返回一个新的GlobalConfig对象,运行以上程序,打印结果如下.
在这里插入图片描述
由此可见,反序列化打破了单例类只能有一个实例的约定。为了避免这一问题,可以在GlobalConfig类中再增加一个readResolve()方法:
在这里插入图片描述
如果一个类提供了readResolve()方法,那么在执行反序列化操作时,先按照默认方式或者用户自定义的方式进行反序列化,最后再调用readResolve()方法,该方法返回的对象为反序列化的最终结果。

提示:readResolve()方法用来重新指定反序列化得到的对象,与此对应,Java序列化规范还允许在可序列化类中定义一个writeReplace()方法,用来重新指定被序列化的对象。writeReplace()方法返回一个Object类型的对象,这个返回对象才是真正要被序列化的对象。writeReplace()方法的访问权限也可以是private、默认或protected级别。

3、实现Externalizable接口

Externalizable接口继承自Serializable接口。如果一个类实现了Externalizable接口,那么将完全由这个类控制自身的序列化行为。Externalizable接口中声明了两个方法:
在这里插入图片描述
writeExternal()方法负责序列化操作,readExternal()方法负责反序列化操作。在对实现了Externalizable接口的类的对象进行反序列化时,会先调用类的不带参数的构造方法,这是有别于默认反序列化方式的。

注意:一个类如果实现了Externalizable接口,那么它必须具有public类型的不带参数的构造方法,否则这个类无法反序列化。

4、可序列化类的不同版本的序列化兼容性

假定Customer5类有两个版本1.0和2.0,如果要把基于1.0的序列化数据反序列化为2.0的Customer5对象,或者把基于2.0的序列化数据反序列化为1.0的Customer5对象,那么会出现什么情况呢?如果可以成功地反序列化,则意味着不同版本之间对序列化兼容,反之,则意味着不同版本之间对序列化不兼容。

凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态常量:

private static final long serialVersionUID;

以上serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码做了修改,再重新编译,那么新生成的类文件的serialVersionUID的取值有可能也会发生变化。

在Customer5类的1.0版本中,具有name和age属性:
在这里插入图片描述
而在Customer5类的2.0版本中,删除了age属性,并且增加了isMarried属性:
在这里插入图片描述
分别对以上两个类编译,把它们的类文件分别放在server和client目录下,此外把SimpleServer和SimpleClient的类文件也分别拷贝到server和client目录下,如下图所示:
在这里插入图片描述
JDK安装好以后,在它的bin目录下有一个serialver.exe程序,用于查看实现了Serializable接口的类的serialVersionUID。在server目录下运行命令“serialver Customer5”,打印结果如下:
在这里插入图片描述
client目录下运行命令“serialver Customer5”,打印结果如下:
在这里插入图片描述
由此可见,Customer5类的两个版本有着不同的serialVersionUID。

先在server目录下运行命令“java SimpleServer Customer5”,然后在client目录下运行命令“java SimpleClient”。SimpleServer按照Customer5类的1.0版本对一个Customer5对象进行序列化,而SimpleClient按照Customer5类的2.0版本进行反序列化,由于两个类的版本不一样,SimpleClient在执行反序列化操作时,会抛出以下异常:
在这里插入图片描述
类的serialVersionUID的默认值依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显式地定义serialVersionUID,为它赋予明确的值。显式定义serialVersionUID有两种用途:

  • (1)在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID。
  • (2)在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

(2)在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

用serialVersionUID来控制序列化兼容性的能力是很有限的。当一个类的不同版本的serialVersionUID相同时,仍然有可能出现序列化不兼容的情况。因为序列化兼容性不仅取决于serialVersionUID,还取决于类的不同版本的实现细节和序列化细节。所以需要前面介绍的各种方法,来手工控制序列化以及反序列化的行为,从而保证不同版本之间的兼容性。

5、总结

如果采用默认的序列化方式,只要让一个类实现Serializable接口,它的实例就可以被序列化了。尽管让一个类变为可序列化很容易,似乎不会给程序员增加很多编程负担,仍然要谨慎地考虑是否要让一个类实现Serializable接口,因为给可序列化类进行版本升级时,需要测试序列化兼容性,这种测试工作量与“可序列化类的数目”与“版本数”的乘积成正比。通常,专门为继承而设计的类应该尽量不要实现Serializable接口,因为一旦父类实现了Serializable接口,所有子类也都变为可序列化的了,这大大增加了为这些类进行升级时测试序列化兼容性的工作量。

默认的序列化方式尽管方便,但是有以下不足之处:

  • (1)直接对对象的不易对外公开的敏感数据进行序列化,这是不安全的。
  • (2)不会检查对象的成员变量是否符合正确的约束条件。
  • (3)默认的序列化方式需要对对象图进行递归遍历,如果对象图很复杂,会消耗很多空间和时间,甚至引起Java虚拟机的堆栈溢出。
  • (4)使类的接口被类的内部实现所束缚,制约类的升级与维护。

为了克服默认序列化方式的不足之处,可以采用以下两种方式控制序列化的行为:

  • (1)可序列化类不仅实现Serializable接口,并且提供private类型的writeObject()和readObject()方法,由这两个方法负责序列化和反序列化。
  • (2)可序列化类不仅实现Externalizable接口,并且实现writeExternal()和readExternal()方法,由这两个方法负责序列化和反序列化。这种可序列化类必须提供public类型的不带参数的构造方法,因为反序列化操作会先调用类的不带参数的构造方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/207102.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode 每日一题 Day 6(DFS+BFS)

1466. 重新规划路线 n 座城市,从 0 到 n-1 编号,其间共有 n-1 条路线。因此,要想在两座不同城市之间旅行只有唯一一条路线可供选择(路线网形成一颗树)。去年,交通运输部决定重新规划路线,以改变…

c语言词法分析器

词法分析器(也称为词法解析器或词法扫描器)是编译器的一个组成部分,它的任务是将输入的源代码(字符流)分解成称为“标记”的序列,其中每个标记对应于源代码中的一个单词或符号。 以下是一个简单的C语言词法…

12月7日作业

使用QT模仿一个登陆界面(模仿育碧Ubisoft登录界面) #include "myqq.h"MyQQ::MyQQ(QWidget *parent): QMainWindow(parent) {this->resize(880,550); //设置窗口大小this->setFixedSize(880,550); //固定窗口大小this->setStyleShee…

android 蓝牙开关设置

frameworks/base/packages/SettingsProvider/res/values/defaults.xml <bool name"def_bluetooth_on">false</bool>将 def_bluetooth_on 的值设为false&#xff08;系统默认开启值&#xff09; adb动态设置 关闭&#xff1a;adb shell settings put gl…

【华为OD题库-082】TLV解析II-Java

题目 两端通过TLVQ格式的报文来通信&#xff0c;现在收到对端的一个TLV格式的消息包&#xff0c;要求生成匹配后的(tag,length,valueOffset)列表。具体要求如下: (1)消息包中多组tag、length、value紧密排列&#xff0c;其中tag,length各占1字节(uint8),value所占字节数等于len…

SpringMVC 案例

文章目录 前言1. 计算器1.1 准备前端代码1.2 测试前端代码1.3 完成后端代码1.4 验证程序 2. 留言板2.1 前端代码准备2.2 测试前端代码2.3 完成前后端交互代码2.4 完成后端代码2.5 案例测试2.6 完善前后端交互2.7 完善后端代码2.8 完整功能测试 lombok简单的方式添加Lombok工具3…

vue3使用mitt用于组件之间传值

vue3已经没有提供配套的事件总线bus&#xff0c;需要使用第三方库mitt来完成vue2中bus完成的事情 1.安装 npm install mitt2.引用 bus.js import mitt from mitt; const bus mitt(); export default bus;3.在需要使用的vue文件中导入bus import bus from ./mitt4.使用mitt…

HarmonyOS学习 第1节 DevEco Studio配置

俗话说的好&#xff0c;工欲善其事,必先利其器。我们先下载官方的开发工具DevEco Studio. 下载完成后&#xff0c;进行安装。 双击DevEco Studio&#xff0c;点击Next按照指引完成安装 重新启动DevEco&#xff0c;点击 Agree 进入环境配置&#xff0c;安装Node.js和ohpm 点击Ne…

MQTT 协议入门:轻松上手,快速掌握核心要点

文章目录 什么是 MQTT&#xff1f;MQTT 的工作原理MQTT 客户端MQTT Broker发布-订阅模式主题QoS MQTT 的工作流程开始使用 MQTT&#xff1a;快速教程准备 MQTT Broker准备 MQTT 客户端创建 MQTT 连接通过通配符订阅主题发布 MQTT 消息MQTT 功能演示保留消息Clean Session遗嘱消…

【WPF】使用ObservableCollection解决:累积计数x与实际计数x不相同

使用观察模式和集合 错误代码 public List<IPAddress> iPAddressDevices new List<IPAddress>();public List<IPAddress> IPAddressDevices { get > iPAddressDevices; set {iPAddressDevices value;RaisePropertyChanged(nameof(IPAddressDevices));…

spring IOC介绍

spring的Ioc真是个好东西啊&#xff0c;那它到底是什么东西呢&#xff0c;控制反转&#xff0c;到底是怎么转的呢&#xff1f; 假设啊你现在是一个导演&#xff0c;想排部戏&#xff0c;那是不是得需要演员和舞台(spring中的bean)&#xff0c;如果按平常的编程思维就是new 一个…

vue-baidu-map实现在地图上选择范围并解决相关问题

vue-baidu-map实现在地图上选择范围并解决相关问题 实现地图上选择不规则范围实现功能遇到的问题1、覆盖物多边形怎么才能盖住覆盖物点2、遇到其他问题 实现地图上选择不规则范围 这个功能比较简单&#xff0c;只需要使用vue-baidu-map插件的覆盖物多边形功能就行了。直接看文…

Win10专业版找不到安全中心选项的解决方法

在Win10电脑中&#xff0c;安全中心功能起到很大的作用。但是&#xff0c;有用户在Win10专业版电脑上找不到安全中心选项&#xff0c;从而影响到自己的正常使用。下面小编分享解决Win10专业版系统没有安全中心的简单方法&#xff0c;解决后在Win10专业版就能成功找到安全中心了…

哈希表及其基础(java详解)

目录 一、哈希表基础 二、哈希函数的设计 哈希函数的设计原则 三、java中的hashCode 基本数据类型的hashCode使用 自定义类型的hashCode使用 需要注意 四、哈希冲突的处理 链地址法Seperate Chaining 五、实现属于我们自己的哈希表 六、哈希表的动态空间处理和复杂…

通过项目管理软件监管新员工入职流程的方法与策略

项目管理软件是什么&#xff1f;项目管理软件都能做什么&#xff1f;是不是只有项目团队需要啊&#xff1f;NO&#xff01;项目管理软件乍听其名不免让人觉得这不过是个项目领域的专用工具。 那什么是项目呢&#xff1f;项目是为创造独特的产品、服务或成果而进行的体系化的工…

物联网+AI智慧工地云平台源码(SaaS模式)

智慧工地云平台充分运用数字化技术&#xff0c;聚焦施工现场岗位一线&#xff0c;依托物联网、互联网、AI等技术&#xff0c;围绕施工现场管理的人、机、料、法、环五大维度&#xff0c;以及施工过程管理的进度、质量、安全三大体系为基础应用&#xff0c;实现全面高效的工程管…

stm32中滴答定时器与普通定时器的区别

1、两者在单片机中的位置不一样 滴答定时器在内核上&#xff0c;普通定时器在外设上。 由于位置不同&#xff0c;滴答定时器的程序可以移植到所有相同内核的芯片上&#xff0c;但普通定时器的程序却不可以。 2、两者的中断优先级不一样 滴答定时器优先级高&#xff0c;普通定…

CTF刷题记录

刷题 我的md5脏了KFC疯狂星期四坤坤的csgo邀请simplePHPcurl 我的md5脏了 g0at无意间发现了被打乱的flag&#xff1a;I{i?8Sms??Cd_1?T51??F_1?} 但是好像缺了不少东西&#xff0c;flag的md5值已经通过py交易得到了&#xff1a;88875458bdd87af5dd2e3c750e534741 flag…

关于微信/支付宝等平台验签/签名sign生成算法

引言 我们在日常工作中经常会遇到对接微信平台、支付宝平台、或者自己对外开放一个api服务&#xff0c;那么这里经常会出现一个名字&#xff1a;sgin&#xff08;签名&#xff09;。 举个栗子 这是微信支付统一下单接口文档&#xff0c;最简单的理解就是&#xff0c;服务端为…

Unirest-Java:Java发起GET、POST、PUT、DELETE、文件上传,文件下载工具类介绍

一、简介 Unirest-Java是一个轻量级的HTTP客户端库&#xff0c;用于在Java应用程序中发送HTTP请求。 它提供了简单易用的API&#xff0c;可以方便地处理GET、POST、PUT、DELETE等HTTP方法。 Unirest-Java支持异步和同步请求&#xff0c;可以轻松地与JSON、XML等数据格式进行…