倚楼听风雨
淡看江湖路

Java中I/O输入输出流之对象序列化浅析

对象序列化的目标(或者说为什么要有序列化这种东西)及意义: 对象序列化的目标是将对象保存到磁盘中或者允许在网络中可以直接传输对象。将对象序列化以后,无论是磁盘存储还是网络传输,对方拿到序列化之后的二进制流都可以进行反序列化从而将其恢复成原来的Java对象继而进行相应的业务逻辑。我们都知道在Java EE(Java Enterprise Edition,Java的企业版,一般就是我们所说的Java web)开发平台中,RMI(Java Remote Method Invocation,Java远程方法调用)技术是Java EE的基础,然而RMI过程中的参数以及返回值都离不开对象的序列化,所以序列化的意义是: 对象序列化机制是Java EE平台的基础。通常我们编写JavaBean的时候理应都实现序列化接口。

为了让某个类是可序列化的,可以实现如下两个任意一个接口即可:

  1. Serializable
  2. Externalizable

我们主要使用Serializable接口,实现该接口无须任何实现方法,它只是标明该类的实例是可序列化的。对象的序列化与反序列化相辅相成,所以我们也是对称着来说,接下来我们通过具体的例子来演示对象序列化的相关知识。

上面就是一个基本对象序列化与反序列化的简单示例,我们看到还是比较简单的。接下来我们分析一下相对复杂的一点的对象序列化,即: 对象引用的序列化。如果Person类的成员变量中类型包含另一个引用类型,我们要求这个引用类型也必须是是可序列化的。当对引用类型对象序列化的时候,对象里面引用类型也会进行序列化操作。这个时候你可能会问了,如果多个类包含同一个引用类型,那会这个引用类型会被序列化多次吗?尝试着思考一下,JDK肯定不会这样来设计。Java序列化机制采用了一种特殊的序列化算法,这种算法保证了对象的序列化只被序列一次,详细算法内容如下:

  1. 所有保存到磁盘中的对象都有一个序列化编号。
  2. 当程序试图序列化一个对象时,程序先检查该对象是否已经被序列化过,只有当该对象在本次虚拟机中没有被序列化过的时候,系统才会将其转换成字节序列输出到硬盘上面
  3. 如果已经序列化过,程序只是直接输出一个序列化编号,不重新序列化该对象

我们举例来说明一下上述引用对象的序列化过程,如下是一个Teacher对象,他引用了之前我们创建的Person对象,然后我们创建两个Teacher的实例,依次将Teacher的两个实例以及Person对象进行序列化操作。具体的反序列化老四就不写了,读者可以亲自去验证一下反序列化之后得到的Person对象是不是同一个引用,继而验证一下序列化的算范是否正确。这里再多说一嘴,就是这样的序列化算法导致我们无法进行动态对象的序列化,因为它只序列化一次,之后就只返回一个序列化编号。比如说上述代码中被注释掉的哪行代码即使我们释放开,你也获取不到name为”老四”的实例变量值。所以当我们使用Java序列化机制序列化可变对象时一定要注意,只有第一次调用写入方法输出对象时才会将对象转换成字节序列,后面的过程中即使对象的实例变量发生了改变,序列化内容也不会被改变。

自定义对象序列化操作

自定义序列化可以满足我们不将所有的类中实例变量全都序列化(如果不做限制,Java的序列化是将类的实例变量全部进行序列化的),这种需求是经常有的,抑或某个引用对象可能真的就不需要序列化,我们就应该满足这种需求,将其排除序列化的范围之外。最简单的自定义排除某个实例变量不需要序列化的操作是使用transient关键字,transient关键字只能修饰实例变量,虽然说有局限性,但是因为简单方便,使用的场景也蛮多的。但是transient关键字因为属于屏蔽实例变量,他也会导致反序列化的时候回复得到的Java对象无法取得该实例变量的值,所以使用的时候也要注意这一点。transient使用方式如下代码所示:

除了使用transient关键字以外,Java还提供了一种自定义序列化机制,通过这种自定义序列化机制可以让程序控制如何序列化。这种自定义序列化要求类里面处理如下三个方法:

  • private void writeObject() throws IOException
  • private void readObject() throws IOException,ClassNotFoundException
  • private void readObjectNoData() throws ObjectStreamException

我们主要使用前两个,一个读(反序列化),一个写(序列化),最后一个是被用于当序列化列不完整或者版本不一致的时候用来正确的初始化反序列化对象的,关于版本概念的讲述会在后面说明。

我们依然使用Person类来说明一下这个自定义序列化如何实现。代码如下:

我们看到新的Person类跟之前的Person类多加了两个方法,这就是我们实现的自定义的序列化操作,我们可以在里面进行对实例变量进行各种业务处理相关操作,不但灵活,也提高了序列化的安全性。

还有一种更彻底更变态的自定义序列化机制操作,它甚至可以在序列化对象的时候将原来的对象替换成其他对象进行序列化操作: writeReplace() throws ObjectStreamException方法。只要存在该方法,在序列化某个对象之前就会调用这个方法进行处理,在这个方法中我们可以将对象进行转换,下面的代码演示了这样的操作。

运行以上代码输出: 读取到的人的对象内容: [高老四, 51],验证了之前的说法,除此之外还有一个readResolve()方法与writeReplace()相对应,在反序列化之后(readObject()之后)被调用,它会返回应该得到的没被writeReplace()替换之前的序列化对象,这里也不再赘述。

说了很多自定义序列化,其实除了以上这些我们涉及到的自定义序列化,Java本身也提供了一个自定义序列化的接口,就是文章开篇列举的Externalizable接口,这种序列化机制也是由我们程序员来决定如何存储和恢复对象,定义了一下两个方法:

  • void readExternal(ObjectInput in);
  • void writeExternal(ObjectOutput out);

与之前讲的非常类似,所以老四不举例说明了,这种实现Externalizable接口的自定义序列化方式虽然和我们手工的自定义序列化没什么太大的区别,但是性能较高,此外需要注意的是: 该接口必须声明无参构造器,否则会导致反序列化失败,你看到老四前面自定义序列的写法中是不需要写无参构造器的,但是Externalizable接口不可以!!!由于这个接口只声明两个方法,间接导致我们编程复杂度的增加,不像Serializable接口我们知道实现声明即可,所以我们平时都是采用实现Serializable接口方式实现序列化

对象序列化的总结以及注意点:

  1. 对象的类名、实例变量(包括基本类型、数组、对其他对象的引用)都会被序列化;方法、类变量(static关键字修饰的成员变量)、transient关键字修饰的实例变量(也叫作瞬间实例变量)都不会被序列化
  2. 实现Serializable接口的类如果需要自定实例变量不被序列化,可以使用transient关键字修饰,虽然static也能达到目的,但不允许这样用,static也不是为了序列化设计的。
  3. 对象中存在引用类型,要保证该引用类型也是可序列化的,要不然当前类声明是可序列化的也不能进行序列化操作。
  4. 反序列化对象时必须有序列化对象的class文件。
  5. 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序进行读取

再讲一下Java对象序列化中版本的概念,从本文中老四的代码,你可以看到,凡是声明对象时可序列化的时候老四都加了一个生成的serialVersionUID静态常量,这个就是版本号,版本的设计是为了解决两个class文件的兼容性。serialVersionUID用于表示该Java类的版本,如果这个类升级之后,如果版本号不变,序列化机制会把他们当成同一个序列化版本,这样反序列化的时候就会根据版本号来保证兼容性,数值可以自己定义,如果出现序列化版本不兼容而无法正确反序列化,我们需要重新为serialVersionUID分配新值。

注意: 类修改了方法不需要修改版本,因为方法不进行序列化操作,其余同理。但是如果修改了transient这样的实例变量皆可能需要修改版本确定序列化与反序列化的兼容性。

更博不易,如果觉得文章对你有帮助并且有能力的老铁烦请赞助盒烟钱,点我去赞助。或者扫描文章下面的微信/支付宝二维码打赏任意金额,老四这里抱拳了。赞助时请备注姓名或者昵称,因为您的署名会出现在赞赏列表页面,您的赞赏钱财也会被用于小站的服务器运维上面,再次抱拳。

赞(24) 给你买杜蕾斯
本站原创文章受自媒体平台原创保护,未经允许不得转载高老四博客 » Java中I/O输入输出流之对象序列化浅析

开始你的表演 2

  1. #1

    不错哟!

    博客大全6年前 (2018-10-09)回复
    • 如果添加友链,老四很愿意合作

      Glorze6年前 (2018-10-09)回复

觉得文章有用就打赏一下老四,鼓励我更好的创作

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫打赏

微信扫一扫打赏

登录

找回密码

注册