谈谈Java序列化与深拷贝

本文深入探讨了Java序列化,包括序列化的概念、版本号问题、序列化内容和算法,以及如何通过重写readObject和writeObject方法实现自定义序列化。此外,还讨论了深拷贝,讲解了clone()方法、数组拷贝和对象数组的深拷贝策略,最后总结了Java序列化的原理和重要知识点。
摘要由CSDN通过智能技术生成

太难了,光搁那复习多线程和集合了,结果面试现在都挑序列化问

序列化理解

我们对序列化做一个最直观的理解,就是把有结构的对象去拉成一个无结构的序列或者某种特定编码的二进制,这个序列可以被用于保存在磁盘上,也可以通过网卡发送到另一台主机上。不管是持久化还是网络传输,我们最终的目标一定是一个字节流。因此序列号的目的就是把有结构的对象转换为无结构的字节流。java为我们提供了序列化和反序列化的抽象,即为objectOutputStream和objectInputStream。

Java序列化

java的序列化非常简单,因为java仅仅为我们提供了两个类型和一个接口。objectOutputStream和objectInputStream就是java为我们提供的序列化和反序列化的工具,而我们如果想要对某一个对象进行序列化,只需要为这个对象标识serializable接口即可。
serializable仅仅是一个起了标识作用的接口,它声明了一种可序列化的类型,而readObject中会做一个转型操作,将传入的对象统一转型为serializable类型。如果无法转型即实现Serializable接口的话,在序列化时就会抛NotSerializableException异常(不是能够序列化的)。
(字符串、数组和枚举都是可序列化的)

基本Java中所有官方提供的对象都实现了serializable接口,因此如果使用serializable作为方法重载的入参,那么所有对象都能够被匹配。

版本号问题

这里还需要提到一个序列化版本号的东西,它可以被看作对一个序列化模板的唯一标识,可以这么理解,一个对象从序列化为一个无结构字节流,再到重新反序列化为原来的对象,这不是通过构造方法进行的,而JVM进行实现的,具体如何实现下面再讨论。
反序列化时,JVM根据字节流中的序列号id与将要被反序列化成的目标类的序列号ID进行对比,只有一致才能支持反序列化。而反序列的过程其实一个对象深拷贝的过程。

我们当前的待反序列化的字节流中序列号为A,而我们希望最终生成一个目标对象,而这个对象的类的序列号是B,进行反序列化操作时,JVM会对两个序列号A和B进行对比,只有一致才能进行反序列化操作

这是一个校验机制,可以保证一个对象即使在网络或磁盘中停留过一次,仍保证数据的正确(能够正确地被还原为一个对象)。实现了类的一致性

序列化版本号JVM会帮我们默认生成,我们也可以自己指定或者使用IDE进行自动生成。推荐后者,如果我们让JVM为我们默认生成,如果我们对序列化对应的模板类进行更改,序列号也会随着发生改变。
“改变”包括方法签名、修饰符、字段、类名、继承关系等结构上的变化,如果内容(方法的内容、属性的默认值等)、顺序(声明的顺序)、空格的修改并不会新生成序列号(已经经过实验)。这里我做一个基于个人的理解:只有当影响到class文件本身结构发生的变化的修改,才会使得生成一个新的版本号

序列化内容

序列化大致存放的就是三类:对象本身的类型、对象成员字段的类型、当前对象各个成员的值
更细一点:瞬态关键字修饰、静态关键字修饰(因为它不属于实例)、方法包括构造函数、final修饰的变量不会被序列化(会重新计算)

java序列化仅保存对象的属性值和类型,因为类是对象的模板,而为属性进行不同的赋值可以对应一个对象,而方法仅仅是一堆字节码指令而已,是不带状态的,方法代表本质上还是对象共用一套模板,是状态的。只有能够拿到某一个类型,classLoader将这个类加载到内存,很容易获得这个类里面有哪些方法。方法的签名等信息在JVM自动生成序列号的时候会进行一个参考

Transient 关键字的作用是控制变量的序列化——瞬态,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值。

显示声明序列号,我们的反序列化便实现了版本向上兼容的功能,允许使用V1版本的应用访问了V2版本的对象,提高了代码的健壮性。此时虽然生产者和消费者对象的类版本不一样,但是显示声明的序列号相同,反序列化也是可以运行的,所带来的业务问题就是消费者不能读取到的新增加的业务属性(还原为零值)

序列化还存在父类与子类之间的问题。
序列化和可以和clone()进行一个类别,如果不光当前对象是serializable,成员对象也需要是serializable的。同时,一个对象的成员不光是自己声明的,从父类继承的成员也属于它的一部分。因此,如果父类实现了序列化接口,那么子类也间接实现了这个接口。而如果子类实现了这个接口,但是父类并没有实现这个接口,那么从父类继承下来从成员不会被序列化
另外,父类必须有无参构造函数(除非父类没有实现serializable接口),因为反序列化时,往往是从超类对象开始构造,这时就会反射调用空参构造器先拿到一个空对象,然后再慢慢地为填充成员注入值(可以类比Spring Bean的创建过程)

注意:final变量不会被保存到序列化流中,它的内容总是被重新计算的。以下三种情况,Final变量不会被赋值:
【1】通过构造方法为final变量赋值
【2】通过方法返回值为final变量赋值
【3】Final修饰的属性不是基本类型

反序列化的执行过程是这样的:反序列化的过程中,构造函数不会执行。JVM从数据流中获取一个Object对象,然后根据数据流中的类文件描述信息(例如Person类),查看发现是final变量(例如name),需要重新计算,于是引用了Person类中的name值,而此时JVM又发现name竟然没有赋值于是不会再初始化,保持原值状态

Java序列化算法

【1】所有保存到磁盘的对象都有一个序列化版本号
【2】当试图序列化一个对象时,会先检查该对象是否被序列化过,只有未被JVM序列化过的对象,才会将该对象序列化为字节序列保存起来
【3】如果该对象已经被序列化过,直接保存序列化编号即可。例如A对象已经被序列化过了,而B对象的一个成员注入了对象A,那么当B序列化时直接保存成员A的序列号

该算法的出发点是节省磁盘空间,当同一个对象被连续序列化时,第二次仅仅会保存一份引用,而不是对元数据和数值进行再次存储。

这里也存在出现问题的风险:
由于序列化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号(编号对应仍是修改前对象的字节序列,因此当还原这个对象的时候仅仅是初始版本的对象

反序列化时,必须有序列化对象的class文件。
数组不能显示的声明UID,因为它们始终有默认的计算值,但是对于数组类,无需匹配UID。
如果一个类的成员不是基本类型或string类型,则成员必须实现序列化接口,否则该类无法序列化.

注意:持久化完毕的对象包含两部分:类描述信息和实例变量(类型、值),因此一个持久化后的对象比class文件要大一些

重写readObject和writeObject方法

反序列化其实是一个隐式的对象构造过程,我们希望“受控”,则可以选择自行编写readObject函数(重写),用于对象的反序列化构造,从而提供约束性。readObject和writeObject方法会在进行反序列化和序列化时被分别通过反射调用,其中ArrayList就重写了自己的两个方法,并且使用瞬态关键字修饰了底层数组,防止过多不必要的空间被序列化。(也就是说,ArrayList告诉JVM:我不需要你赋值序列化,我自己来就行)

    private void writeObject(ObjectOutputStream out
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值