Java Serialization序列化机制-学习笔记

本文是周一到周三整理的关于Java序列化机制的笔记,大部分内容是从《Java网络编程精解》上摘记的,还有一些来自自己的补充内容。内容比较多,算作是version1.0的笔记,方便日后复习。

文章是从word上直接copy过来的,编辑器支持不太好,有些地方看起来不太舒服,请不要太介意~

                                                                                                            2013.3.30

———————————————————————————————————————

Serialization序列化

一、什么是序列化

   广义上的序列化指将内存中的各种数据(文本、图片、语音、视频等)通过一定策略转换成字节序列的形式,方便保存到数据库或硬盘,或者进行网络数据通信。而反序列化就是将序列化后的字节序列恢复为原来的数据。

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

 

二、序列化能干嘛?为什么需要序列化?

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

   ①把对象的字节序列保存在硬盘(通常是一个文件)或数据库中;

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

 

三、JDK类库中的的序列化API

1.对象输出流java.io.ObjectOutputStream

   它的void writeObject(Object obj)方法可对参数obj对象进行序列化将得到的字节序列输出到一个目标输出流创建ObjectOutputStream时指定中。

2.对象输入流java.io.ObjectInputStream

   它的Object readObject()方法从一个源输入流创建ObjectInputStream时指定中读取字节序列再把它们反序列化成一个对象并将其返回。

3.序列化相关接口

   实现了SerializableExternalizable接口的类被称为可序列化类。只有可序列化类才能被序列化,否则ObjectOutputStreamwriteObject(Object obj)方法会抛出IOException

   实现Serializable接口的类可以采用默认的序列化方式。

   实现Externalizable接口的类完全由自身控制序列化的行为。

4.可序列化类的序列化和反序列化操作方式

1)只实现了Serializable接口

>>  ObjectOutputStream采用默认的序列化方式,将该可序列化类的transient实例变量进行序列化;

>>  ObjectInputStream采用默认的反序列化方式,将该可序列化类的非transient实例变量进行反序列化。

2)只实现了Serializable接口,同时定义了readObject(ObjectInputStream in)writeObject(ObjectOutputStream out)方法:

>>  ObjectOutputStream会调用writeObject(ObjectOutputStream out)方法进行序列化;

>>  ObjectInputStream会调用readObject(ObjectInputStream in)方法进行反序列化。

3)实现了Externalizable接口(需要实现writeExternal(ObjectOutput out)方法和readExternal(ObjectInput in)方法):

>>  ObjectOutputStream会调用writeExternal(ObjectOutput out)方法进行序列化;

>>  ObjectInputStream会调用readExternal(ObjectInput in)方法进行反序列化。

 

5.对象的序列化和反序列化的步骤

1)对象序列化

① 创建一个ObjectOutputStream,它包装一个其他类型的目标输出流,如文件输出流FileOutputStream

② 通过ObjectOutputStreamwriteObject()方法进行序列化,写到目标输出流。

2)对象反序列化

① 创建一个ObjectInputStream,它包装一个其他类型的目标输入流,如文件输入流FileInputStream

② 通过ObjectInputStreamreadObject()方法进行反序列化,读取对象并返回。

 

***注意:为了保证能读出正确的数据,必须保证向ObjectOutputStream写对象的顺序与从ObjectInputStream读对象的顺序一致。

 

6.默认序列化和反序列化

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

2)默认情况下,ObjectInputStream按照默认方式反序列化,有以下特点:

①如果在内存中对象所属的类还没有加载,那么会先加载并初始化这个类(初始化常量、静态变量,并会调用该类的静态代码块)。如果在classpath中不存在相应的类文件,那么会抛出ClassNotFoundException

②默认反序列化不会调用类的任何构造方法。

 

7.关于默认序列化和反序列化的补充说明

1)默认序列化不会序列化静态变量,执行默认反序列化时会执行静态数据块。若本地之前已经存在当前类的实例,不会改变原本存在的静态变量;

2Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用和一些控制信息。反序列化时,恢复引用关系,使得两次恢复所得到的引用指向同一对象,二者相等。需要注意的还有,当第二次以后将同一对象写入同一输出流时,虚拟机根据引用关系知道已经有一个相同对象被写入了,因此第二次只保存指向第一次写入的对象的引用,哪怕在第一次序列化之后进行过变量修改,这种修改也不会被保存。

 

8.关于transient变量在序列化操作中的说明

   如果一个实例变量被transient关键字修饰,那么默认序列化不会对它序列化。据此可以用transient来修饰一下类型的实例变量:

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

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

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

 

9.具有关联关系的可序列化类的默认序列化

1)可序列化类的关联类也要实现Serializable接口,否则抛出NoSerializableException

2)可序列化类的对象图是指,通过对该可序列化类进行递归遍历能够到达的所有对象构成的集合。如果对象图很复杂,递归遍历需要消耗许多时间和空间,还有可能发生OutOfMemoryError

3)在默认方式下,ObjectOutputStream会对整个对象图进行序列化,ObjectInputStream会对整个对象图进行反序列化;

 

10.自定义序列化的行为

1)默认序列化方式尽管方便,但有以下缺点:

①直接对对象的不宜对外公开的敏感数据进行序列化,这不安全;

②不会检查对象的成员变量是否符合正确的约束条件;

③默认的序列化方式需要对对象图进行递归遍历,如果对象图很复杂,会消耗很多空间和时间,甚至引起JVM的堆栈溢出;

④使类的接口被类的内部实现所束缚,制约类的升级与维护。

 

2)自定义序列化行为

   在可序列化类中实现下面writeObject()方法readObject()方法(可单选):

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

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

    在自定义的writeObject()方法中,可以先调用ObjectOutputStreamdefaultWriteObject()方法,使得对象输出流先执行默认的序列化操作。

    在自定义的readObject()方法中,可以先调用ObjectInputStreamdefaultReadObject()方法,使得对象输入流先执行默认的反序列化操作。

 

3)之所以不把writeObject()方法和readObject()方法放在Serializable接口中,是因为这么做具有以下优点:

①不必公开这两个方法的访问权限,以便封装序列化的细节。如果把这两个方法放在Serializable接口中,就必须定义为public类型;

②不必强迫用户定义的可序列化类实现这两个方法。如果把这两个方法放在Serializable接口中,它的实现类就必须实现这些方法,否则就只能声明为抽象类。

 

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

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

    某些对象中包含着敏感信息(如银行口令,账号密码),不宜对外公开。如果按照默认方式序列化,在序列化字节序列在网络上进行传输时可能被窃取。对于此类信息,可以将敏感信息进行加密后再序列化,在反序列化时则需要解密,再恢复为原信息。

②确保对象的成员变量符合正确的约束条件;

    通常,在一个类的构造方法中,会对用于赋值给成员变量的参数进行合法性检查,而默认的序列化方式不会调用类的构造方法,直接由对象的序列化数据来构造出一个对象。在序列化数据进行网络传输时,可能被不法分子篡改序列化数据,使得在默认反序列化构造一个不符合约束条件的对象。此时就可以增加readObject()方法,在其中进行合法性检查,防止意外情况的发生。

③优化序列化的性能;

    这是为了防止默认序列化方式对整个对象图的递归遍历造成的资源消耗或堆栈溢出。通过仅仅保存与对象内部数据结构无关的关键数据,在反序列化时再利用已有方法进行复杂对象的生成,能节省空间和时间,提高序列化的性能。

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

    默认的序列化方式会原封不动地对一个对象的内部数据结构进行序列化,这会使类的接口被类的内部实现所束缚。而采用自定义序列化,用户可以定义不涉及内部数据结构的序列化方式,因而不会由于类内部实现更新而使问题更加复杂。

 

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

   使用默认序列化方式可能造成单例类有多个实例的错误。因为无论采用默认方式,还是采用用户自定义的方式,反序列化都会创建一个新的对象。在可序列化单例类中添加readResolve()方法可以防止单例模式被破坏。

private Object readResolve() throws ObjectStreamException {

   return instance;}

   readResolve()用来重新制定反序列化得到的对象,先前按照默认反序列化或用户自定义反序列化创建的对象将被丢弃。

**由于readResolve()是单例类的方法,instance就是原来单例类中的那个唯一的实例,所以单例不会被破坏。

**readResolve()方法应该能够被类本身、同一个包中的类,或者子类访问,因此readResolve()方法的访问权限可以是private、默认或protected级别。

 

11.Externalizable接口

   Externalizable接口继承自Serializable接口。实现Externalizable接口的类将完全由这个类控制自身的序列化行为。Externalizable接口包括两个方法:

public void writeExternal(ObjectOutput out) throws IOException

public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException

1)writeExternal()方法负责序列化操作;

2)readExternal()方法负责反序列化操作,该操作会先调用类的静态代码块,再调用类的无参构造函数这是与默认序列化方式的最大区别。

 

***注意:一个实现了Externalizable接口的可序列化类,必须具有public类型的无参构造函数,否则在对该可序列化类进行反序列化时会抛出java.io.InvalidClassException

 

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

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

private static final long serialVersionUID;

* 上面的serialVersionUID是由Java运行时环境根据类的内部细节自动生成的。对于同一个类在不同时间下、用不同的编译器编译都有可能导致不同的serialVersionUID也有可能相同。

 

强烈建议在一个可序列化类中显式地定义serialVersionUID并赋予明确的值,好处:

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

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

 

***注意:用serialVersionUID类控制序列化兼容性的能力是很有限的。当一个类的不同版本的serialVersionUID相同,仍然有可能出现序列化不兼容的情况。因为序列化兼容性不仅取决于serialVersionUID,还取决于类的不同版本的实现细节和序列化细节。

 

你可能感兴趣的:(java,序列化)