https://cloud.tencent.com/developer/news/276874
所谓的『JAVA对象序列化』就是指,将一个JAVA对象所描述的所有内容以文件IO的方式写入二进制文件的一个过程。关于序列化,主要涉及两个流,ObjectInputStream和ObjectOutputStream。
稍显复杂的main函数:
可以看到,这种古老的序列化方式其实就是使用流DataInput/OutputStream将对象中字段的值逐个的写入文件,完成所谓的『序列化操作』。恢复对象的时候也必须按照写入的顺序一个字段一个字段的读取,这种方式可以说非常的反人类了,如果一个类有一百个字段,岂不是得手动写入一百次。这种方式准确意义上来说并不能算作『序列化』的一种实现,它是一种伪序列化,大家知道一下就好了。
输出结果:single23ObjectOutputStream某种意义上来看也是一种装饰者流,内部所有的字节流操作都依赖我们构造实例时传入的OutputStream实例。这个类的实现很复杂,光内部类就定义了很多,同时它也封装了我们的DataOutputStream,所以DataOutputStream那一套写基本数据类型的方法,这里也有。
序列化后的对象需要用这么多的二进制位进行存储,这些二进制位都是符合JAVA的序列化规则的,每几个字节用来存储什么都是规定好的,下面我们一起来看看。1、魔数:这个是几乎所有的二进制文件头部都有的,用于标识当前二进制文件的文件类型,我们的对象序列化文件的魔数是ACED,占两个字节。
注意,字符串和数组类型并没有划分到普通的Java对象这一类中,它们具有不同的数值标志。我们这里的People是一个普通的Java对象,所以这里是0x73。4、一个字节:这一个字节指明当前的对象所属的数据类型,是一个类或者是一个引用,这里的引用区别于Java的引用指针。
6、序列号版本:接下来的八个字节,3A->B5描述的是当前类对象的序列化版本号,这个值由于我们定义的People类中没有显式指明,所以编译器会根据People类的相关信息以某种算法生成一个serialVersionUID占八个字节。7、序列化类型:一个字节,用于指明当前对象的序列化类型,0x02即代表当前对象可序列化。
9、字段类型:一个字节,0x4C对应的ASCII值为L,即表示当前字段的类型是一个普通类类型。10、字段名长度:两个字节,0x0003指明接下来的三个字节表述了当前字段的全名称,0x616765正好对应字符age。
12、字段描述结束符:一个字节,固定值0x78标志所有的字段类型信息描述结束。13、父类类型描述:一个字节,0x70代表null,即没有父类,不算Object类。接下来这一段其实是Java序列化一个Integer对象的过程,然后到0x7872,即Integer类还有父类,于是又去序列化一个父类Number实例。
最后一个0x7870,说明所有的对象信息都已经序列化完成,下面是各个字段的数据部分。前四个字节,0x00000017是我们第一个字段age的值,也就是23。0x74指明第二个字段的类型是String类型,值的长度0x0006,最后六个字节刚好是字符串single。
这两个类的定义几乎就是相同的,内部都定义了一个People字段。
让ClassA和ClassB的两个对象公用同一个People实例,那么有一个问题,我去序列化这两个对象,这个公用的People对象会被序列化两次吗?我们打开二进制文件,这次的二进制文件要复杂一点了:
我圈出来了几个0x7870,它标志着一个对象类型信息的序列化结束,我们简单分析一下,不会详细的说了,具体参照上面的内容。第一部分其实是在序列化ClassA类型,它指明了ClassA类型只有一个字段,并且该字段是一个对象类型,记录下字段的类型名称等信息。第二部分在序列化People类型,包括序列化其中的name字段,并存储了name字段的外部赋的值,字符串:single。
其中,阴影部分是ClassB类的全限定名,红线框是该类的版本序列号,由于我们没有显式指定,这是由编译器自动生成的。接着指明具有一个字段,字段类型是对象类型,名称长度六个字节。0x71指明这个字段是一个引用,按惯例来说,这部分应该进行该字段的类型名称描述,但是由于这种类型已经序列化过了,所以使用引用直接指向前面已经完成序列化的People类型。
name被关键字transient修饰,即默认的序列化机制不会序列化该字段,并且我们重写了writeObject和readObject,在其中调用了默认的序列化方法之后,我们分别将name字段写入和读出。