Java序列化基础
本文Ctrl-V自《Java编程思想》第四版第X章Y小节。
序列化简介
Java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。这一过程甚至可以通过网络进行,这意味着序列化机制能自动弥补不同操作系统之间的差异。也就是说,可以在运行Windows系统的计算机上创建一个对象,将其序列化,通过网络将它发送给一台运行Unix系统的计算机,然后在那里准确地重新组装,而却不必担心数据在不同机器上的表示会不同,也不必关心字节的顺序或者其他任何细节。
就其本身来说,对象的序列化是非常有趣的,因为利用它可以实现轻量级持久性(lightweight persistence)。“持久性”意味着一个对象的生命周期并不取决于程序是否正在执行;它可以生存于程序的调用之间。通过将一个序列化对象写入磁盘,然后在重新调用程序时恢复该对象,就能够实现持久性的效果。之所以称其为“轻量级”,是因为不能用某种“persistent”(持久)关键字来简单地定义一个对象,并让系统自动维护其他细节问题(尽管将来有可能实现)。相反,对象必须在程序中显式地序列化(serialize)和反序列化还原(deserialize)。如果需要一个更严格的持久性机制,可以考虑像Hibernate之类的工具。
对象序列化的概念加入到语言中时为了支持两种主要特性。一是Java的远程方法调用(Remote Method Invocation,RMI),它使存活于其他计算机上的对象使用起来就像是存活于本机上一样。当向远程对象发送消息时,需要通过对象序列化来传输参数和返回值。
再者,对Java Beans来说,对象的序列化也是必需的。使用一个Bean时,一般情况下是在设计阶段对它的状态信息进行配置。这种状态信息必需保存下来,并在程序启动时进行后期恢复;这种具体工作就是由对象序列化完成的。
只要对象实现了Serializable接口(该接口仅是一个标记接口,不包括任何方法),对象的序列化处理就会非常简单。当序列化的概念被加入到语言中时,许多标准类库都发生了改变,以便具备序列化特性——其中包括所有基本数据类型的封装器、所有容器类以及许多其他的东西。甚至Class对象也可以被序列化。
要序列化一个对象,首先要创建某些OutputStream对象,然后将其封装在一个ObjectOutputStream对象内。这时,只需调用writeObject()即可将对象序列化,并将其发送给OutputStream(对象化序列是基于字节的,因此要使用InputStream和OutputStream继承层次结构)。要反向进行该过程(即将一个序列还原为一个对象),需要将一个InputStream封装在ObjectInputStream内,然后调用readObject()。和往常一样,我们最后获得的是一个引用,它指向一个向上转型的Object,所以必须向下转型才能直接设置它们。
对象序列化特别“聪明”的一个地方是它不仅保存了对象的“全景图”,而且能追踪对象内所包含的所有引用,并保存那些对象;接着又能对对象内包含的每个这样的引用进行追踪;以此类推。这种情况有时被称为“对象网”,单个对象可与之建立连接,而且它还包含了对象的引用数组以及成员对象。如果必须保持一套自己的对象序列化机制,那么维护那些可追踪到所有链接的代码可能会显得非常麻烦。然而,由于Java的对象序列化似乎找不出什么缺点,所以请尽量不要自己动手,让它用优化的算法自动维护整个对象网。
真正的序列化过程却是非常简单地。一旦从另外某个流创建了ObjectOutputStream,writeObject()就会将对象序列化。注意也可以为一个String调用writeObject()。也可以用与DataOutputStream相同的方法写入所有基本数据类型(它们具有同样的接口)。
注意在对一个Serializable对象进行还原的过程中,没有调用任何构造器,包括默认的构造器。整个对象都是通过从InputStream中取得数据恢复而来的。
注意:被还原的类Class对象,必须在当前的类路径classpath下。
序列化的控制
如果有特殊的需要,可通过实现Externalizable接口——代替实现Serializable接口——来对序列化过程进行控制。这个Externalizable接口继承了Serializable接口,同时增添了两个方法:writeExternal()和readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊操作。
对于Serializable对象,对象完全以它存储的二进制位为基础来构造,而不调用构造器。而对于一个Externalizable对象,所有普通的默认构造器都会被调用(包括在字段定义时的初始化),然后调用readExternal()。
必须注意这一点——所有默认的构造器都会被调用,才能使Externalizable对象产生正确的行为。
如果我们从一个Externalizable对象继承,通常需要调用基类版本的writeExternal()和readExternal()来为基类组件提供恰当的存储和恢复功能。
因此,为了正常运行,我们不仅需要在writeExternal()方法(没有任何默认行为来为Externalizable对象写入任何成员对象)中将来自对象的重要信息写入,还必须在readExternal()方法中恢复数据。起先,可能会有一点迷惑,因为Externalizable对象的默认构造行为使其看起来似乎像某种自动发生的存储与恢复操作。但实际上并非如此。
transient(瞬时)关键字
当我们对序列化进行控制时,可能某个特定子对象不想让Java的序列化机制自动保存与恢复。如果子对象表示的是我们不希望将其序列化的敏感信息(如密码),通常就会面临这种情况。即使对象中的这些信息是private(私有)属性,一经过序列化处理,人们就可以通过读取文件或者拦截网络传输的方式来访问到它。
有一种办法可防止对象的敏感部分被序列化,就是讲类实现为Externalizable,这样一来,没有任何东西可以自动序列化,并且可以再writeExternal()内部只对所需部分进行显式的序列化。
然而,如果我们正在操作的是一个Serializable对象,那么所有序列化操作都会自动进行。为了能够予以控制,可以用transient(瞬时)关键字逐个字段地关闭序列化,它的意思是“不用麻烦你保存或恢复数据——我自己会处理的”。
由于Externalizable对象在默认情况下不保存它们的任何字段,所以transient关键字只能和Serializable对象一起使用。
Externalizable的替代方法
如果不是特别坚持实现Externalizable接口,那么还有另外一种方法。我们可以实现Serializable接口,并添加(注意我说的是“添加”,而非“覆盖”或者“实现”)名为writeObject()和readObject()的方法。这样一旦对象呗序列化或者被反序列化还原,就会自动地分别调用这两个方法。也就是说,只要我们提供了这两个方法,就会使用它们而不是默认的序列化机制。
这些方法必须具有准确地方法签名特征:
private void writeObject(ObjectOutputStream stream) throws IOException;
private void readObject(ObjectInputStream stream) throws IOException,
ClassNotFoundException;
从设计的观点来看,现在事情变得真是不可思议。首先,我们可能会认为由于这些方法不是基类或者Serializable接口的一部分,所以应该在它们自己的接口中进行定义。但是注意它们被定义成了private,这意味着它们仅能被这个类的其他成员调用。然而,实际上我们并没有从这个类的其他方法中调用它们,而是ObjectOutputStream和ObjectInputStream对象的writeObject()和readObject()方法调用你的对象的writeObject()和readObject()方法。读者可能想知道ObjectOutputStream和ObjectInputStream对象是怎样访问你的类中的private方法的。(反射)
在接口中定义的所有东西都自动式public的,因此如果writeObject()和readObject()必须是private的,那么它们不会是接口的一部分。因为我们必须要完全遵循其方法特征签名,所以其效果就和实现了接口一样。
在调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象,看看是否实现了它自己的writeObject()。如果是这样,就跳过正常的序列化过程并调用它的writeObject()。readObject()的情形与此相同。
还有另外一个技巧。在你的writeObject内部,可以调用defaultWriteObject()来选择执行默认的writeObject()。类似地,在readObject()内部,我们可以调用defaultReadObject()。
如果我们打算使用默认机制写入对象的非transient部分,那么必须调用defaultWriteObject()作为writeObject()中的第一个操作,并让defaultReadObject()作为readObject()中的第一个操作。这些都是奇怪的方法调用。
版本控制
有时可能想要改变可序列化类的版本(比如源类的对象可能保存在数据库中)。虽然Java支持这种做法,但是你可能只在特殊的情况下才这样做,此外,还需要对它有相当深度的了解(在这里我们就不再试图达到这一点)。从http://java.sun.com处下载的JDK文档中对这一主题进行了非常彻底的论述。
我们会发现在JDK文档中有许多注解是从下面的文字开始的:
警告:该类的序列化对象和未来的Swing版本不兼容。当前对序列化的支持只适用于短期存储或应用之间的RMI。
这是因为Java的版本控制机过于简单,因而不能再任何场合都可靠运转,尤其是对JavaBeans更是如此。
适用“持久性”
一个比较诱人的使用序列化技术的想法是:存储程序的一些状态,以便我们随后可以很容易地将程序恢复到当前状态。但是在我们能够这样做之前,必须回答几个问题。如果我们将两个对象——它们都具有指向第三个对象的应用——进行序列化,会发生什么情况?当我们从它们的序列化状态恢复这两个对象时,第三个对象会只出现一次么?如果将这两个对象序列化成独立的文件,然后在代码的不同部分对它们进行反序列化还原,又会怎样呢?
只要将任何对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,并且没有任何意外重复复制出的对象。当然,我们可以在写出第一个对象和写出最后一个对象期间改变这些对象的状态,但是这是我们自己的事;无论对象在被序列化时处于什么状态(无论它们和其他对象有什么样的连接关系),它们都可以被写出。
如果我们想保存系统状态,最安全的做法是将其作为“原子”操作进行序列化。如果我们序列化了某些东西,再去做其他一些工作,再来序列化更多的东西,如此等等,那么将无法安全地保存系统状态。取而代之的是,将构成系统状态的所有状态都置入单一容器内,并在一个操作中将该容器直接写出。然后同样只需一次方法调用,即可以将其恢复。