对象的序列化与反序列化

来源:互联网 发布:台湾现状知乎 编辑:程序博客网 时间:2024/06/06 01:18
 

JAVA虚拟机在内存中分了四个部分,包括stack segment,Heap segment,code segment, data segment。

其中我们程序中用关键字new出来的东西都是存放在heap segment;程序中的局部变量存放在stack segment, 这些局部变量是在具体方法执行结束之后,系统自动释放内存资源(而heap segment中的资源需要java垃圾回收机制来处理)。

程序中的方法,是在内存中的code segment中的,而且是多个对象共享一个代码空间区域;

static静态变量,需要存放在内存中data segment中的。

当两个Java进程进行远程通信时,一个进程能否把一个Java对像发送给另一个进程呢?答案是肯定的。不过,发送发余姚把这个Java对象转换为字节序列,才能在网络上传送;接收方则要把字节序列再恢复为Java对象。

把Java对象转换为字节序列的过程称为对象的序列化;把字节序列恢复为Java对象的过程称为对象的反序列化。

当程序运行时,程序所创建的各种对象都位于内存中,当程序运行结束,这些对象就结束生命周期。

对象的序列化主要有两种用途:

1)    把对象的字节序列永久的保存到硬盘上,通常存放一个文件中。

2)    在网络上传送对象的字节序列。

java.io.ObjectOutputSteam代表输出流,它的writeObject(Object obj)方法对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

java.io.ObjectInputStream代表对象输入流。它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化成一个对象,并将其返回。

只有实现了Serializable 或Externalizable接口的类对象才能被序列化,否则ObjectOutputSteam的writeObject(Object obj)方法会抛出IOException。

实现Serializable或Externalizable接口的类也称为可序列化的类。

Externalizable接口继承自Serializable接口,实现Externalizable接口的类万元由自身来控制序列化行为,而仅实现Serialiable接口的类可以采用默认的序列化方式。

JDK类库中的部分类(如String类、包装类和Date类等都实现了Serializable接口)

 

JDK类库中序列化的主要包含以下步骤。

1)    创建一个对象输出流,它可以包装一个其它类型的目标输出流,比如文件输出流:

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(“D;//objectFile.obj”));

2)    通过对象输出流的writeObject()方法写对象:

out.writeObject(“hello”);  //写一个String对象

out.writeObject(new Date());  //写一个Date对象

 

对象的反序列化主要包含以下步骤。

1)    创建一个对象输入流,它可以包装一个其它类型的源输入流,比如文件输入流:

ObjectInputStream out = new ObjectInputStream(new FileInputStream(“d://objetFile.obj”));

2)    通过对象输入流的readObject()方法读取对象;

String obj1 = (String)out.readObject();  // 读取一个String对象

Date obj2 = (Date)out.readObject();  // 读取一个Date对象

为了能读取正确的数据,必须保证向对象输入流写对象的顺序与从对象输入流读取对象的顺序一致。

 

默认情况下,ObjectOutputStream按照默认方式序列化,这种序列化方式仅仅对对象的非transient的实例变量进行序列化,而不会序列化对象的transient的实例变量,也不会序列化静态变量。

 

当ObjectInputStream按照默认方式序列化时,有以下特点:

如果内存中对象所属的类还没有被加载,那么先加载并初始化这个类。如果在classpath中不存在相应的类文件,那么会抛出ClassNotFoundException。

在反序列化时不会调用类的任何构造方法。

如果一个实例变量被transient修饰符修饰,那么默认的序列化方式不会对它序列化。根据这一特点,可以用transient修饰符来修饰以下类型的实例变量:

1)    实例变量不代表对象的固有的内部数据,仅仅代表具有一定逻辑含义的临时数据。

2)    实例变量表示一些比较敏感的信息(比如银行账户的口令),出于安全的原因,不希望对其序列化。

3)    实例变量需要按照用户按照用户自定义的方式序列化,比如经过加密后在序列化。在这种情况下,可以把实例变量定义为transient类型,然后在writeObject()方法中对其序列化。

 

序列化对象图:

类与类之间可能存在关联关系。当通过ObjectOutputStream对象的writeObject(customer)方法序列化Customer对象时,会不会序列化与它关联的Order对象呢?答案是肯定的。

在默认方式下,对象输出流会对整个对象图进行序列化,当程序执行writeObject(customer)方法时,该方法不仅序列化Customer对象,还会把两个与它关联的Order对象也进行序列化。

当通过ObjectInputStream对象的readObject()方法反序列化Customer对象,实际上会对整个对象图反序列化。

 

控制序列化的行为:

如果用户希望控制类的序列化方式,可以在可序列化中提供以下形式的writeObject()方法和readObject()方法:

private void writeObject(java.io.ObjectOutputStream out) throws IOException

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException

n 当ObjectOutputStream对一个Customer对象进行序列化时,如果该Customer对象具有writeObject()方法,那么就会执行这一方法,否则就按默认方式序列化。

n 在Customer对象的writeObject()方法中,可以先调用ObjectOutputStream的defaultWriteObject()方法,使得对象输出流先执行默认的序列化操作。

 

 

在以下情况,可以考虑采用用户自定义的序列化方式,从而控制序列化的行为:

1)    确保序列化的安全性,对敏感的信息加密后再序列化,在反序列化时则需要解密。

2)    确保对象的成员变量符合正确的约束条件。

3)    优化序列化的性能。

4)    便于更好的封装累的内部数据结构,确保类的接口不会被类的内部实现所束缚。

 

readResolve()方法在单例类中的运用。

单例类是指仅有一个实例的类。在系统中具有惟一性的组件可作为单例类,这种类的实例通常会占用较多的内存,或者实例的初始过程比较冗长,因此随意创建这些类的实例会影响系统的性能。

如果一个类提供了readResolve()方法,那么在执行反序列化操作时,先按照默认方式或者用户自定义的方式进行反序列化,最后再调用readResolve()方法,该方法返回的对象为反序列化的最终结果。

实现Externalizable接口:

Externalizable接口继承自Serializable接口。如果一个类实现了Externalizable接口,那么将完全由这个类控制自身的序列化行为。Externalizable接口中声明了两个方法:

public void writeExternal(ObjectOutput out) throws IOException

public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException

 

writeExternal()方法负责序列化操作,readExternal()方法负责反序列化操作。在对实现了Externalizable接口的类的对象进行反序列化时,会先调用类的不带参数的构造方法,这是有别于默认反序列化方式的。

由此可见,一个类如果实现了Externalizable接口,那么它必须具有public类型的不带参数的构造方法,否则这个类无法反序列化。

可序列化类的不同版本序列化兼容性

假定Customer类有两个版本1.0和2.0,如果要把基于1.0的序列化为2.0的Cusomer对象,或者把基于2.0的序列化数据反序列化为1.0的Customer对象,会出现什么情况呢?如果可以成功反序列化,则意味着不同版本之间对序列化兼容,反之,则意味着不同版本对序列化不兼容。

凡事实现了Serialiazable接口的类都有一个表示序列化版本标识符的静态常量:

private static final long serialVersionUID;

以上serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的,如果对类的源代码做了修改,再重新编译,新生成的类文件的serialiVersionUID的取值可能也会发生变化。

 

n 类的serialVersionUID的默认值依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID,也有可能相同。

n 为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显式的定义serialVersionUID,为它赋予明确的值。

n 显式的定义serialVersionUID有两种用途:

n (1)在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID。

n (2)在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

 

原创粉丝点击