Java序列化拾掇
本来想总结一下对google protobuf的用法总结,然而搜资料的过程中发现对很多java序列化的知识不足,故做了一些拾掇,在flink中关于类型序列化的地方其实也涉及很多,待以后看到,如有新的思考再来补充。
java序列化
- 在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化
- 若父类未实现Serializable,而子类序列化了,父类属性值不会被保存,反序列化后父类属性值丢失
- 通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化
- 在deserialized的时候类的构造器是不会被调用的,只会调用没有实现Serializabe接口的父类的无参构造方法,如果其父类不可序列化,并且没有无参构造函数就会导致
InvalidClassException - 只有non-static的成员并且没有标记为transient才会被序列化
- 类所包含的成员变量也必须是可序列化的
- transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null
- java的serialVersionUID用来表明类的不同版本间的兼容性,必须被定义成final static long才能生效,否则会报错
- 在使用Externalizable进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,实现Externalizable接口的类必须要提供一个public的无参的构造器,如果一个Java类没有定义任何构造函数,编译器会帮我们自动添加一个无参的构造方法,可是,如果我们在类中定义了一个有参数的构造方法了,编译器便不会再帮我们创建无参构造方法,这点需要注意
- 用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程
serialVersionUID
Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来 的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序 列化,否则就会出现序列化版本不一致的异常。
当实现java.io.Serializable接口的实体(类)没有显式地定义一个名为serialVersionUID,类型为long的变 量时,Java序列化机制会根据编译的class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,只有同一次编译生成的 class才会生成相同的serialVersionUID 。
如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,未作更改的类,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行串行化和反串行化。
在内部类使用中带来的困扰
A class that is serializable with an enclosing class that is not serializable causes serialization to fail.
Non-static nested classes that implement Serializable must be defined in an enclosing class that is also serializable. Non-static nested classes retain an implicit reference to an instance of their enclosing class. If the enclosing class is not serializable, the Java serialization mechanism fails with a java.io.NotSerializableException.
一个非静态的内部类实现了Serializable接口,要求其外部类也同样实现Serializable接口。这是因为一个非静态内部类包含有一个隐式的指向外部包装类实例对象的一个指针,如上面指出的规则,序列化的时候要求类的非静态成员也需要是可序列化的,如果外部类没有声明Serializable,java序列化机制就会报错,解法通常是
- 将内部类声明为static,这样就不包含隐式指针了
- 将外部类声明为Serializable
在flink中采用了另一种解法,用户通过匿名内部类来定义一个userFuntion,通常userFunction需要被序列化来分发到各个task节点来执行,定义成static不如匿名类方便,外部主类定义成Serializable的代价又比较大,因此采用另一种解法:
1 | for (Field f: cls.getDeclaredFields()) { |
对每一个userFunction(可能实现自一个匿名内部类)有一个clean的机制。
- 检查其声明字段有没有
this$开始的,即指向外部类的引用 - 如果有将对应的字段通过反射置成null,这样就不会受第三条规则的困扰了
自定义序列化
在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。并且这两个方法的signature必须是以下这样才会生效,否则就是默认的序列化方式
1 | private void readObject(java.io.ObjectInputStream in) |
如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。这两个方法没有覆写,也没有被显式调用,为什么会生效呢? 具体可以参见https://mp.weixin.qq.com/s/ABtxdNpr4bLpXtFiOK47hA
在使用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject方法时,会通过反射的方式调用
关于序列化的困惑可以在这个源码中得到解答
ObjectOutputStream#writeObject
writeObject => writeObject0 => writeOrdinaryObject => writeSerialData => invokeWriteObject
可以参见ArrayList使用了这种自定义序列化的方法,ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient。
Ps:在两个方法的开始处,你会发现调用了defaultWriteObject()和defaultReadObject()。它们做的是默认的序列化进程,就像写/读所有的non-transient和 non-static字段(但他们不会去做serialVersionUID的检查).通常说来,所有我们想要自己处理的字段都应该声明为transient。这样的话,defaultWriteObject/defaultReadObject便可以专注于其余字段,而我们则可为这些特定的字段(译者:指transient)定制序列化。使用那两个默认的方法并不是强制的,而是给予了处理复杂应用时更多的灵活性
关于反序列化的时候构造方法是否会被调用(反序列化是怎么做的)
A non-serializable, immediate superclass of a serializable class that does not itself declare an accessible, no-argument constructor causes deserialization to fail
To allow subtypes of non-serializable classes to be serialized, the subtype may assume responsibility for saving and restoring the state of the supertype’s public, protected, and (if accessible) package fields. The subtype may assume this responsibility only if the class it extends has an accessible no-arg constructor to initialize the class’s state. It is an error to declare a class Serializable if this is not the case. The error will be detected at runtime.
反序列化其实是将先前序列化生成的byte流重新构建成一个对象,byte流包含了所有重构对象的信息,包括class的元数据,实例的变量的类型信息,以及相应的值。然后在反序列化的时候它要求all the parent classes of instance should be Serializable; and if any super class in hirarchy is not Serializable then it must have a default constructor。在反序列化的时候会一直搜寻其父类,直到找到第一个不可序列化的类,就尝试调用其无参构造函数创建对象,如果所有的父类都是可序列化的,最终找到的就是Object类,然后首先创建一个Object对象。接着JVM就继续读取byte流,设置相关的类型信息,一个空对象创建完成后,jvm就设置相关的static字段,并调用readObject方法进行赋值
由于本类的构造方法不会被调用,所以你期望某个变量的初始化在构造方法中完成得到的只会是null。
在构造函数的调用上Externalizable和Serializable的表现不同,Externalizable依赖于本身类的无参构造函数
利用序列化来做deepCopy
主要用到了ByteArrayOutputStream,存储在内存中,不做持久化
1 | public SerializableClass deepCopy() throws Exception{ |
更多关于java clone 可参考:
https://howtodoinjava.com/core-java/cloning/a-guide-to-object-cloning-in-java/
如果对象状态需要同步,则对象序列化也需要同步
1 | private synchronized void writeObject(ObjectOutputStream s) throws IOException { |
单例模式序列化
参考:
- http://www.hollischuang.com/archives/1144
- 枚举实现可序列化单例 http://www.cnblogs.com/cielosun/p/6596475.html
- https://leokongwq.github.io/2017/08/21/why-enum-singleton-are-serialization-safe.html
在不同的classloader之间进行对象的序列化和反序列化
如上所说,在同一个classloader中,利用如下的方法serializabale和deserializable对象:
1 | ByteArrayOutputStream bo=new ByteArrayOutputStream(); |
当序列化的对象和反序列化的对象不在同一个classloader中时,以上的代码执行时,就会报无法把属性付给对象的错误,此时应当,通过设置反序列化得classloader,来解决这个问题。
首先,从ObjectInputStream继承一个自己的ObjectInputStream
1 | public class CustomObjectInputStream extends ObjectInputStream { |
比较重要的是这里的resolveClass方法传入classloader,反序列化时将classloader传入
1 | ByteArrayOutputStream bo=new ByteArrayOutputStream(); |
序列化相关方法
writeObject、readObject、readObjectNoData、writeReplace和readResolve 待补充