Java序列化机制的深入研究


1、java序列化简介

序列化就是指对象通过写出描述自己状态的数值来记录自己的过程,即将对象表示成一系列有序字节,java提供了将对象写入流和从流中恢复对象的方法。对象能包含其它的对象,而其它的对象又可以包含另外的对象。JAVA序列化能够自动的处理嵌套的对象。对于一个对象的简单域,writeObject()直接将其值写入流中。当遇到一个对象域时,writeObject()被再次调用,如果这个对象内嵌另一个对象,那么,writeObject() 又被调用,直到对象能被直接写入流为止。程序员所需要做的是将对象传入ObjectOutputStream writeObject() 方法,剩下的将有系统自动完成。

要实现序列化的类必须实现的java.io.Serializablejava.io. Externalizable接口,否则将产生一个NotSerializableException。该接口内部并没有任何方法,它只是一个"tagging interface" ,仅仅"tags"它自己的对象是一个特殊的类型。类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。Java"对象序列化"能让你将一个实现了Serializable接口的对象转换成一组byte,这样日后要用这个对象时候,你就能把这些byte数据恢复出来,并据此重新构建那个对象了。

序列化图示

反序列化图示

在序列化的时候,writeObjectreadObject之间是有先后顺序的。readObject将最先writeobject read出来。用数据结构的术语来讲就称之为先进先出!

2、序列化的必要性及目的

Java中,一切都是对象,在分布式环境中经常需要将Object从这一端网络或设备传递到另一端。这就需要有一种可以在两端传输数据的协议。Java序列化机制就是为了解决这个问题而产生。

Java序列化支持的两种主要特性:

Java RMI使本来存在于其他机器的对象可以表现出就象本地机器上的行为。

将消息发给远程对象时,需要通过对象序列化来传输参数和返回值.

Java序列化的目的:

支持运行在不同虚拟机上不同版本类之间的双向通讯;

定义允许JAVA类读取用相同类较老版本写入的数据流的机制;

定义允许JAVA类写用相同类较老版本读取的数据流的机制;

提供对持久性和RMI的序列化;

产生压缩流且运行良好以使RMI能序列化;

辨别写入的是否是本地流;

保持非版本化类的低负载;

3、序列化异常

    序列化对象期间可能抛出6种异常:

InvalidClassException  通常在重序列化流无法确定类型时或返回的类无法在取得对象的系统中表示时抛出此异常。异常也在恢复的类不声明为public时或没有public缺省(无变元)构造器时抛出。

NotSerializableException  通常由具体化对象(负责自身的重序列化)探测到输入流错误时抛出。错误通常由意外不变量值指示,或者表示要序列化的对象不可序列化。

StreamCorruptedException  在存放对象的头或控制数据无效时抛出。

OptionalDataException  流中应包含对象但实际只包含原型数据时抛出。

ClassNotFoundException  流的读取端找不到反序列化对象的类时抛出。

IOException  要读取或写入的对象发生与流有关的错误时抛出。

4、序列化一个对象

序列化一个对象,以及对序列化后的对象进行操作,需要遵循以下3点:

1、 一个对象能够序列化的前提是实现Serializable接口或Externalizable接口,Serializable接口没有方法,更像是个标记。有了这个标记的Class就能被序列化机制处理。

2、 写个程序将对象序列化并输出。ObjectOutputStream能把Object输出成Byte流。

3、 要从持久的文件中读取Bytes重建对象,我们可以使用ObjectInputStream。 

在序列化时,有几点要注意的:

l  当一个对象被序列化时,只序列化对象的非静态成员变量,不能序列化任何成员方法和静态成员变量。

l  如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存。

l 如果一个可序列化的对象包含对某个不可序列化的对象的引用,那么整个序列化操作将会失败,并且会抛出一个NotSerializableException。可以通过将这个引用标记为transient,那么对象仍然可以序列化。对于一些比较敏感的不想序列化的数据,也可以采用该标识进行修饰。

5、对象的序列化格式

5.1 简单对象的序列化介绍

package com.asc.alibaba.base;

 

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectOutputStream;

import java.io.Serializable;

 

publicclassTestSerial implements Serializable{

   

    publicbyteversion = 100;

    publicbytecount = 0;

   

    publicstaticvoid main(String[] args) throws IOException, ClassNotFoundException {

        FileOutputStream fos = new FileOutputStream("temp.out");

        ObjectOutputStream oos = new ObjectOutputStream(fos);

        TestSerialize testSerialize = new TestSerialize();

        oos.writeObject(testSerialize);

        oos.flush();

        oos.close();

        }

    }

}

将一个对象序列化后是什么样子呢?打开刚才将对象序列化输出的temp.out文件,以16进制方式显示。内容应该如下:

AC ED 00 05 73 72 00 0A 53 65 72 69 61 6C 54 65
73 74 A0 0C 34 00 FE B1 DD F9 02 00 02 42 00 05
63 6F 75 6E 74 42 00 07 76 65 72 73 69 6F 6E 78
70 00 64
这一堆字节就是用来描述序列化以后的TestSerial对象的,我们注意到TestSerial类中只有两个域:

public byte version = 100;

public byte count = 0;

且都是byte型,理论上存储这两个域只需要2个byte,但是实际上temp.out占据空间为51bytes,也就是说除了数据以外,还包括了对序列化对象的其他描述。

开头部分,见颜色

²  AC ED: STREAM_MAGIC. 声明使用了序列化协议.

²  00 05: STREAM_VERSION. 序列化协议版本.

²  0x73: TC_OBJECT. 声明这是一个新的对象.  

输出TestSerial类的描述。见颜色:

²  0x72: TC_CLASSDESC. 声明这里开始一个新Class。

²  00 0A: Class名字的长度.

²  53 65 72 69 61 6c 54 65 73 74: TestSerial,Class类名.

²  05 52 81 5A AC 66 02 F6: SerialVersionUID, 序列化ID,如果没有指定,
则会由算法随机生成一个8byte的ID.

²  0x02: 标记号. 该值声明该对象支持序列化。

²  00 02: 该类所包含的域个数。

输出域的信息,见颜色

²  0x42: 域类型. 42 代表"B", 也就是byte;

²  00 05: 域名字的长度;

²  636F 75 6E 74: count,域名字描述count;

²  0x42: 域类型. 42 代表"B", 也就是byte;

²  00 07: 域名字的长度;

²  76 65 72 73 69 6F 6E 78 70: version,域名字描述version;
块的结束标记:见颜色
0x78: TC_ENDBLOCKDATA,对象块结束的标志。

0x70:TC_NULL,没有超类了。

输出域的值信息,见颜色:
²  00: 域值为00;
²  64: 域值为100;

5.2 复杂对象的序列化介绍

package com.asc.alibaba.base;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectOutputStream;

import java.io.Serializable;

 

class Contain implements Serializable {

    publicintcontainVersion = 11;

}

 

class Parent implements Serializable {

    publicintparentVersion = 10;

}

 

publicclass SerialTest extends Parent implements Serializable {

 

    publicint       version   = 66;

 

    public Contain con = new Contain();

 

    publicint getVersion() {

        returnversion;

    }

 

    publicstaticvoid main(String[] args) throws IOException {

        FileOutputStream fos = new FileOutputStream("temp1.out");

        ObjectOutputStream oos = new ObjectOutputStream(fos);

        SerialTest testSerialize = new SerialTest();

        oos.writeObject(testSerialize);

        oos.flush();

        oos.close();

 

        FileInputStream fis = new FileInputStream("temp1.out");

        byte[] bb = newbyte[200];

        while (fis.read(bb) != -1) {

            for (byte b : bb) {

                System.out.print(Integer.toHexString(b));

                System.out.print(" ");

            }

        }

    }

 

}

这个例子中SerialTest类实现了Parent超类,内部还持有一个Container对象。序列化后的格式如下:

AC ED 00 05 73 72 00 0A 53 65 72 69 61 6C 54 65 73 74 05 52 81 5A AC 66 02 F6 02 00 02 49 00 07 76 65 72 73 69 6F 6E 4C 00 03 63 6F 6E 74 00 09 4C 63 6F 6E 74 61 69 6E 3B 78 72 00 06 70 61 72 65 6E 74 0E DB D2 BD 85 EE 63 7A 02 00 01 49 00 0D 70 61 72 65 6E 74 56 65 72 73 69 6F 6E 78 70 00 00 00 0A 00 00 00 42 73 72 00 07 63 6F 6E 74 61 69 6E FC BB E6 0E FB CB 60 C7 02 00 01 49 00 0E 63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E 78 70 00 00 00 0B

开头部分,见颜色

²  ACED: STREAM_MAGIC. 声明使用了序列化协议;

²  0005: STREAM_VERSION. 序列化协议版本;

²  0x73: TC_OBJECT. 声明这是一个新的对象;

输出TestSerial类的描述。见颜色:

²  0x72: TC_CLASSDESC. 声明这里开始一个新Class;

²  000A: Class名字的长度;

²  5365 72 69 61 6c 54 65 73 74: SerialTest,Class类名;

²  0552 81 5A AC 66 02 F6: SerialVersionUID, 序列化ID,如果没有指定,则会由算法随机生成一个8byte的ID;

²  0x02: 标记号. 该值声明该对象支持序列化;

²  0002: 该类所包含的域个数;

输出域的信息,见颜色

²  0x49: 域类型. 49 代表"I", 也就是Int.

²  00 07: 域名字的长度.

²  76 65 72 73 69 6F 6E: version,域名字描述.

算法输出下一个域,contain con = new contain();这个有点特殊,是个对象。描述对象类型引用时需要使用JVM的标准对象签名表示法,见颜色

²  0x4C: 域的类型;

²  0003: 域名字长度;

²  636F 6E: 域名字描述,con;

²  0x74: TC_STRING. 代表一个new String.用String来引用对象;

²  0009: 该String长度;

²  4C63 6F 6E 74 61 69 6E 3B: Lcontain;,JVM的标准对象签名表示法;

²  0x78: TC_ENDBLOCKDATA,对象数据块结束的标志;

算法就会输出超类也就是Parent类描述了,见颜色

²  0x72: TC_CLASSDESC. 声明这个是个新类;

²  00 06: 类名长度;

²  70 6172 65 6E 74: parent,类名描述;

²  0E DBD2 BD 85 EE 63 7A: SerialVersionUID, 序列化ID;

²  0x02: 标记号. 该值声明该对象支持序列化;

²  00 01: 类中域的个数;

输出parent类的域描述,int parentVersion=100;见颜色

²  0x49: 域类型. 49 代表"I", 也就是Int;

²  00 0D: 域名字长度;

²  70 6172 65 6E 74 56 65 72 73 69 6F 6E: parentVersion,域名字描述;

²  0x78: TC_ENDBLOCKDATA,对象块结束的标志;

²  0x70: TC_NULL, 说明没有其他超类的标志;

到此为止,算法已经对所有的类的描述都做了输出。下一步就是把实例对象的实际值输出了。这时候是从parent Class的域开始的,见颜色

²  00 00 00 0A: 10, parentVersion域的值.

²  还有SerialTest类的域:

²  00 00 00 42: 66, version域的值.

再往后的bytes比较有意思,算法需要描述contain类的信息,要记住,
现在还没有对contain类进行过描述,见颜色

²  0x73: TC_OBJECT, 声明这是一个新的对象;

²  0x72: TC_CLASSDESC声明这里开始一个新Class;

²  0007: 类名的长度;

²  636F 6E 74 61 69 6E:contain,类名描述;

²  FCBB E6 0E FB CB 60 C7: SerialVersionUID, 序列化ID;

²  0x02: Various flags. 标记号. 该值声明该对象支持序列化;

²  0001: 类内的域个数;

输出contain的唯一的域描述,int containVersion=11:

²  0x49: 域类型. 49 代表"I", 也就是Int;

²  000E: 域名字长度;

²  636F 6E 74 61 69 6E 56 65 72 73 69 6F 6E: containVersion, 域名字描述;

²  0x78: TC_ENDBLOCKDATA对象块结束的标志;

这时,序列化算法会检查contain是否有超类,如果有的话会接着输出;

²  0x70:TC_NULL,没有超类了;

最后,将contain类实际域值输出:

²  00 00 00 0B: 11, containVersion的值。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 下一页

相关内容