Java的对象序列化是指将那些实现了Serializable接口的对象转换成一个字符序列,并能够在以后将这个字节序列完全恢复为原来的对象。这一过程甚至可通过网络进行,这意味着序列化机制能自动弥补不同操作系统之间的差异。 只要对象实现了Serializable接口(记住,这个接口只是一个标记接口,不包含任何的方法
如果我们想要序列化一个对象,首先要创建某些OutputStream(如FileOutputStream、ByteArrayOutputStream等),然后将这些OutputStream封装在一个ObjectOutputStream中。这时候,只需要调用writeObject()方法就可以将对象序列化,并将其发送给OutputStream(记住:对象的序列化是基于字节的,不能使用Reader和Writer等基于字符的层次结构)。而反序列的过程(即将一个序列还原成为一个对象),需要将一个InputStream(如FileInputstream、ByteArrayInputStream等)封装在ObjectInputStream内,然后调用readObject()即可。
对象序列化过程不仅仅保存单个对象,还能追踪对象内所包含的所有引用,并保存那些对象(这些对象也需实现了Serializable接口)。下面这段代码演示了此过程:
|
|
|
|
|
|
运行结果如下:
思考:
1)反序列化后的对象,需要调用构造函数重新构造吗?
答案:不需要。对于Serializable对象,对象完全以它存储的二进制位作为基础来构造,而不调用构造器
请看下面这段代码
|
|
|
|
运行结果如下所示:
从上面的结果中可以看到,在序列化前,当我们使用
Animal animal = new Animal(“test”,house);
时,调用了Animal的构造器(打印了输出语句),但是反序列后并没有再打印任何语句,说明并没有调用构造器。
2)序列前的对象与序列化后的对象是什么关系?是(“==”还是equal?是浅复制还是深复制?)
答案:深复制,反序列化还原后的对象地址与原来的的地址不同。 我们还是看上面思考1)中给出的代码,
前两个类不变化,修改第三个类(MyWorld.java)的部分代码,修改后的代码如下:
|
|
运行结果如下:
从结果可以看到
序列化前后对象的地址不同了,但是内容是一样的,而且对象中包含的引用也相同。换句话说,通过序列化操作, 我们可以实现对任何可Serializable对象的”深度复制(deep copy)
——这意味着我们复制的是整个对象网,而不仅仅是基本对象及其引用。对于同一流的对象,他们地址是相同,说明他们是同一个对象,但是与其他流的对象地址却不相同。
也就说,只要将对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,而且只要在同一流中,对象都是同一个。
补充:
serialVersionUID 的作用?
- 在Java中,软件的兼容性是一个大问题,尤其在使用到对象串行性的时候,那么在某一个对象已经被串行化了,可是这个对象又被修改后重新部署了,那么在这种情况下, 用老软件来读取新文件格式虽然不是什么难事,但是有可能丢失一些信息。 serialVersionUID来解决这些问题,新增的serialVersionUID必须定义成下面这种形式:static final long serialVersionUID=-2805284943658356093L;。其中数字后面加上的L表示这是一个long值。 通过这种方式来解决不同的版本之间的串行话问题。
Java串行化机制定义的文件格式似乎很脆弱,只要稍微改动一下类的定义,原来保存的对象就可能无法读取。例如,下面是一个简单的类定义:
|
|
如果在这个类定义中增加一个域,例如final int val = 7;,再来读取原来保存的对象,就会出现下面的异常
:
java.io.InvalidClassException:
Save; local class incompatible:
stream classdesc serialVersionUID = -2805284943658356093,
local class serialVersionUID = 3419534311899376629
上例异常信息中的数字串表示类定义里各种属性的编码值:
类的名字(Save)。
域的名字(name)。
方法的名字(Save)。
已实现的接口(Serializable)。
改动上述任意一项内容(无论是增加或删除),都会引起编码值变化,从而引起类似的异常警报。这个数字序列称为“串行化版本统一标识符”(serial version universal identifier),简称UID。
解决这个问题的办法是在类里面新增一个域serialVersionUID,强制类仍旧使用原来的UID。新增的域必须是:
static:该域定义的属性作用于整个类,而非特定的对象。
final:保证代码运行期间该域不会被修改。
long:它是一个64位的数值。
也就是说,新增的serialVersionUID必须定义成下面这种形式:static final long serialVersionUID=-2805284943658356093L;。其中数字后面加上的L表示这是一个long值。
当然,改动之后的类不一定能够和原来的对象兼容。例如,如果把一个域的定义从String改成了int,执行逆-串行化操作时系统就不知道如何处理该值,显示出错误信息:java.io.InvalidClassException: Save; incompatible types for field name。