序列化的意义和价值是什么?
序列化就是把对象转换为字节序列。
反序列化就是把字节序列还原为对象。
对象是程序运行时的信息载体,序列化机制使得对象信息可以脱离程序的运行而独立存在,这样便于进行网络传输或持久性存储到磁盘。
序列化考量的重点是什么?
- 协议是否跨平台
- 序列化速度
- 字节序列大小
Serializable底层运作机制是怎样的?
查看ObjectInputStream的readObject方法 和 ObjectOutputStream的writeObject的代码。
主要运用了反射。
Serializable如何自定义决定哪些字段写入和读取?
- 通过transient关键字修饰属性,排除在序列化之外
- 在类中增加writeObejct和readObject方法,JDK中的ArrayList、HashMap等都复写了writeObject
- 在类中增加writeReplace和readResolve方法,提供了writeReplace就不会执行writeObject,提供了readResolve就不会执行readObject
- 实现Externalizable接口,显式实现readExternal和writeExternal
Java为用户定义了默认的序列化、反序列化方法,其实就是ObjectOutputStream的defaultWriteObject方法和ObjectInputStream的defaultReadObject方法。
为什么HashMap要自定义序列化逻辑呢?
可能的原因是,存储数据的数组table,一般都是不满的(因为HashMap的负载因子默认0.75,超过就会扩容),里面肯定会有很多null,如果是默认的序列化,这些null也会被被序列化,显然这些null是没有必要的做序列化的。
ArrayList自定义序列化方法,也是因为elementData数组可能没存满。
serialVersionUID是干什么的?
反序列化需要指定一个类,但是类的结构会变更,如何保证已经序列化的字节序列与当前的类结构是对应上的,可以在字节序列和类中分别存储一个版本号,只有版本号相同,就当做是可转行的,版本号不同反序列化就报错。
类中指定版本号是使用private static final long serialVersionUID属性,值为任意。如果不声明这个属性,虚拟机会根据类信息自动生成一个版本号。
这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
什么情况下需要修改serialVersionUID呢?
分三种情况:
- 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
- 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
- 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。
Serializable序列化会有什么问题?
当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。
Serializable反序列化会有什么问题?
反序列化不会调用类的构造函数,会调用第一个非序列化父类的无参构造函数
这点可以通过 ObjectInputSteam -> readObject -> readObject0 -> readOrdinaryObject -> desc.newInstance() 得知,desc是ObjectStreamClass的实例,ObjectStreamClass.newInstance()的注释是:
1 | /** |
这样设计的目的是什么?
推测是因为如果调用了类的构造函数,如果构造函数中有改变类属性状态的代码,反序列化后就得不到序列化时的状态了,所以不能调用。
Serializable的具体序列化执行过程中哪些地方用到了大量的反射?
因为没有在代码层面手动写入特定字段,序列化都是全自动化的,在ObjectOutputStream底层都是通过反射获取类的字段信息,defaultWriteFields()读取类的属性字段是通过Field,Field从ObjectStreamClass的getFields()得来,getFields()内部还是反射。
ObjectInputStream也通过反射创建类。
Serializable的具体序列化执行过程中哪些地方创建了大量的临时对象?会有什么问题?
创建大量的临时对象会引发频繁的垃圾回收,垃圾回收会stop the world,进而引起进程的卡顿。
Serializable的序列化过程比较复杂,有很多对象参与工作,例如ObjectStreamClass。
Parceable底层运作机制是怎样的?
Interface for classes whose instances can be written to and restored from a Parcel. Classes implementing the Parcelable interface must also have a non-null static field called CREATOR of a type that implements the Parcelable.Creator interface.
A typical implementation of Parcelable is:
1 | public class MyParcelable implements Parcelable { |
Parcelable通过Parcel的一系列read和write方法实现序列化,Parcel的read和write方法都是在native层实现。
Parcel把对象数据直接序列化输出在内存,并且数据结构很紧凑,几乎只存储数据值,不存储额外的信息,而Serializable会存储很多额外的信息,所以Parcel序列化的输出的信息量就比Serializable少,同时没有用反射,而是直接操作内存指针,因此速度更快。
缺点:
1. 由于数据紧凑,没有存储必要的判断信息,不能用于持久化存储,因为反序列化时,缺乏足够的信息来保证数据恢复的正确性。
2. 需要手动指定哪些字段写入,并且写入顺序和读入顺序要一致
Parceable与Serializable有什么区别?
Serializable 会使用反射,序列化和反序列化过程需要大量 I/O 操作。
Parcelable 自已实现封送和解封(marshalled &unmarshalled)操作不需要用反射,数据也存放在 Native 内存中,效率要快很多。