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++;
    }

    @Override
    public 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.mfbz.cn/a/232173.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

本地部署语音转文字(whisper,SpeechRecognition)

本地部署语音转文字 1.whisper1.首先安装Chocolatey2.安装3.使用 2.SpeechRecognition1.环境2.中文包3.格式转化4.运行 3.效果 1.whisper 1.首先安装Chocolatey https://github.com/openai/whisper 以管理员身份运行PowerShell Set-ExecutionPolicy Bypass -Scope Process -…

自动化测试框架性能测试报告模板

一、项目概述 1.1 编写目的 本次测试报告,为自动化测试框架性能测试总结报告。目的在于总结我们课程所压测的目标系统的性能点、优化历史和可优化方向。 1.2 项目背景 我们公开课的性能测试目标系统。主要是用于我们课程自动化测试框架功能的实现,以及…

记录 | ubuntu监控cpu频率、温度等

ubuntu监控cpu频率、温度等 采用 i7z 进行监控,先安装: sudo apt install i7z -ysudo i7z

基于51单片机的多模式智能闹钟系统【代码+仿真+论文+PPT等16个文件资料】

一、项目功能简介 整个设计系统由STC89C52单片机LCD1602显示模块DS1302模块温度模块存储模块矩阵按键模块组成。 具体功能: 1、智能闹钟正常模式显示阳历年、月、日、星期、小时、分、秒; 2、可设置时间和日期; 3、 LCD显示当前温度&…

游戏玩家升级不伤手之选,光威龙武系列超强性能

得益于国产存储芯片的崛起,现在的内存条价格太香了。要放在前几年,购买内存条时都会优先考虑国际一线品牌。随着内存条行业发生巨变,国产品牌光威GLOWAY,是全球前三的内存模组厂商嘉合劲威旗下品牌,它推出的内存条产品…

十八、FreeRTOS之FreeRTOS任务通知

本节需要掌握以下内容: 1、任务通知的简介(了解) 2、任务通知值和通知状态(熟悉) 3、任务通知相关API函数介绍(熟悉) 4、任务通知模拟信号量实验(掌握) 5、任务通知…

JOSEF约瑟 接触式中间继电器 JZC1-53 AC220V 导轨安装

系列型号 JZC1-22中间继电器;JZC1-44中间继电器; JZC1-62中间继电器;JZC1-80中间继电器; JZC1-71中间继电器;JZC1-53中间继电器; JZC1-32中间继电器;JZC1-40中间继电器; JZC1-31中间…

HarmonyOS4.0从零开始的开发教程10Video组件的使用

HarmonyOS(九)Video组件的使用 概述 在手机、平板或是智慧屏这些终端设备上,媒体功能可以算作是我们最常用的场景之一。无论是实现音频的播放、录制、采集,还是视频的播放、切换、循环,亦或是相机的预览、拍照等功能…

【Python】翻译包translate

在Python里,可以用translate包完成语言的翻译转化 import translatetrantranslate.Translator(from_lang"ZH",to_lang"JA") strtran.translate("今天的天气怎么样")print(str)

四招打造完美分层自动化测试框架,让测试更高效!

写在前面 我们刚开始做自动化测试,可能写的代码都是基于原生写的代码,看起来特别不美观,而且感觉特别生硬。 来看下面一段代码: 具体表现如下: driver对象在测试类中显示 定位元素的value值在测试类中显示 定位元素…

【Vue+Python】—— 基于Vue与Python的图书管理系统

文章目录 🍖 前言🎶一、项目描述✨二、项目展示🏆三、撒花 🍖 前言 【VuePython】—— 基于Vue与Python的图书管理系统 🎶一、项目描述 描述: 本项目为《基于Vue与Python的图书管理系统》,项目…

【出现模块node_modules里面包找不到】

#pic_center R 1 R_1 R1​ R 2 R^2 R2 目录 一、出现的问题二、解决办法三、其它可供参考 一、出现的问题 在本地运行 npm run docs:dev之后,出现 Error [ERR_MODULE_NOT_FOUND]: Cannot find package Z:\Blog\docs\node_modules\htmlparser2\ imported from Z:\Blo…

建行驻江门市分行纪检组以廉政家访助推廉洁家风

为强化员工行为管理,深入了解员工的家庭情况以及员工8小时以外的生活,近日,建行驻江门市分行纪检组组长带队对两名青年纪检员开展廉政家访。 驻行纪检组组长亲切问候并访谈了青年纪检员的家庭成员,详细了解其家庭生活情况&#x…

类的生命周期

加载:通过类的完全限定名,查找此类的二进制字节码文件,利用字节码文件创建Class对象 验证:验证类是否符合JVM规范,安全行检查 文件格式验证、元数据验证、字节码验证。格式检查如:文件格式是否错误、语法是…

【C语言】操作符详解(二)

目录 移位操作符 左移操作符 右移操作符 位操作符:&、|、^、~ 一道面试题 移位操作符 <<左移操作符 >>右移操作符注:移位…

IEEE期刊论文模板

一、模板下载 1、登陆IEEE作者中心Author Center 地址&#xff1a;Publish with IEEE Journals - IEEE Author Center Journals 2、点击“Download a template” 3、在弹出的模板下载页面点击IEEE模板选择器“IEEE Template Selector” 4、在弹出的模板选择器页面点击“Tran…

vivado时序方法检查10

TIMING-41 &#xff1a; 内部管脚上定义的前向时钟无效 前向时钟 <clock_group> 是在管脚 <netlist_element> 上定义的 &#xff0c; 而不是在端口 <netlist_element> 上定义的。 描述 前向时钟是在连接到输出端口的叶节点管脚上定义的 &#xff0c…

计算机毕业设计 基于大数据的心脏病患者数据分析管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

Codeforces Round 913 (Div. 3) A~G

A.Rook&#xff08;循环&#xff09; 题意&#xff1a; 给出一个 8 8 8 \times 8 88的棋盘和一个棋子&#xff08;可以任选上下左右四方向移动任意步数&#xff09;&#xff0c;问一次移动可以到达哪些格子。 分析&#xff1a; 使用for循环对棋子所在的行列进行遍历并输出…

【词云图】从excel和从txt文件,绘制以句子、词为单位的词云图

从excel和从txt文件&#xff0c;绘制以句子、词为单位的词云图 写在最前面数据说明&结论 从txt文件&#xff0c;绘制以句子、词为单位的词云图自我介绍 从excel&#xff0c;绘制以句子、词为单位的词云图读取excel绘制以句子、词为单位的词云图文章标题 写在最前面 经常绘…