1. 什么是Java对象序列化?
Java的对象序列化是将那些实现了Serializable接口的对象转化成一个字节序列,并能够在以后将这些字节序列完全恢复成原来的对象。简单来说序列化就是将对象转化成字节流,反序列化就是将字节流转化成对象。
对象必须在程序中显示的序列化(serialize)和反序列化(deserialize)。
2. 序列化的作用
序列化的主要用途主要有两个,一个是对象持久化,另一个是跨网络的数据交换、远程过程调用。
3. 基本实现
要实现Java对象的序列化,只要将类实现Serializable或Externalizable接口即可。采用类实现Serializable接口的序列化很简单,Java自动会将非transient修饰属性序列化到指定文件中去。
一个简单的将对象写入到文件的方法:
private static void saveObj(Object obj) {
try {
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("obj.out"));
out.writeObject(obj);
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
ObjectOutputStream类的writeObject()代码如下:
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
在继承ObjectOutputStream类的子类中, enableOverride属性为true, 并且会去调用实现的writeObjectOverride()方法. 否则直接调用writeObject0方法, 该方法会先根据obj的Class对象获得一个ObjectStreamClass类实例, 该实例包含了对该obj的描述(包括serialVersionUID, 属性域, 方法等). 根据obj的类型来调用不同的write方法, 当obj是除String, Array, Enum以外的对象时, 会去调用writeOrdinaryObject()方法. 该方法先去调用writeClassDesc()方法, 将ObjectStreamClass对象实例的必要信息写入(这样在从流中反序列化一个对象时, 会根据写入的这些信息实例化出ObjectSteamClass对象)会判断如果obj是Externalizable的子类型, 则调用其obj的writeExternal()和readExternal()方法, 否则调用自身的writeSerialData()方法, 在该方法里会去判断obj是否有writeObject()方法, 如果有, 则会直接调用该方法, 否则调用自身的defaultWriteFields()方法.
调用ObjectInputStream类的readObject()方法和写入过程相反, 先根据其classDesc字段信息, 实例化ObjectStreamClass对象, 然后再实例化特定的对象.
4. 部分属性序列化
如果只想将部分属性进行序列化,可以采用如下几种方法:
1) 使用transient关键字定义不想被序列化的属性
2) 在将要被序列化的类中添加私有的writeObject和readObject方法,这两个方法会被ObjectOutputStream类中的代码通过反射调用
3) 被序列化类实现Externalizable接口,Externalizable 接口继承于Serializable,实现该接口,需要重写readExternal和writeExternal方法。另外,使用 Externalizable 接口进行序列化时,读取对象会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中,这就是为什么在此次序列化过程中Person类的无参构造器会被调用。由于这个原因,实现 Externalizable 接口的类必须要提供一个无参构造器,且它的访问权限为public。
4) 序列化并不保存静态变量
5. 父类及对象引用序列化
要想将父类对象也序列化,就需要让父类也实现 Serializable 接口
若一个类的字段有引用对象,那么在序列化该类的时候不仅该类要实现Serializable接口,这个引用类型也要实现Serializable接口。但有时我们并不需要对这个引用类型进行序列化,此时就需要使用transient关键字来修饰该引用类型保证在序列化的过程中跳过该引用类型。
通过序列化操作,可以实现对任何可 Serializable 对象的深度复制(deep copy),这意味着复制的是整个对象的关系网,而不仅仅是基本对象及其引用。
如果父类没有实现Serializable接口,但其子类实现了此接口,那么这个子类是可以序列化的,但是在反序列化的过程中会调用父类的无参构造函数,所以在其直接父类(注意是直接父类)中必须有一个无参的构造函数。
6. 序列化的安全性
服务器端给客户端发送序列化对象数据,序列化二进制格式的数据写在文档中,并且完全可逆。一抓包就能就看到类是什么样子,以及它包含什么内容。如果对象中有一些数据是敏感的,比如密码字符串等,则要对字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
比如可以通过使用 writeObject 和 readObject 实现密码加密和签名管理,但其实还有更好的方式。如果需要对整个对象进行加密和签名,最简单的是将它放在一个 javax.crypto.SealedObject 或 java.security.SignedObject 包装器中。两者都是可序列化的,所以将对象包装在 SealedObject 中可以围绕原对象创建一种 “包装盒”。必须有对称密钥才能解密,而且密钥必须单独管理。同样,也可以将 SignedObject 用于数据验证,并且对称密钥也必须单独管理
7. ArrayList 序列化要注意的问题
ArrayList实现了java.io.Serializable接口,但是其 elementData 是 transient 的,但是 ArrayList 是通过数组实现的,数组 elementData 用来保存列表中的元素。通过该属性的声明方式知道该数据无法通过序列化持久化。
但是如果实际测试,就会发现,ArrayList 能被完整的序列化,原因是在writeObject 和 readObject方法中进行了序列化的实现。
这样设计的原因是因为 ArrayList 是动态数组,如果数组自动增长长度设为 2000,而实际只放了一个元素,那就会序列化 1999 个 null 元素,为了保证在序列化的时候不会将这么多 null 元素序列化,ArrayList 把元素数组设置为transient,但是,作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化,所以,通过重写 writeObject 和 readObject 方法把其中的元素保留下来,具体做法是:
writeObject方法把elementData数组中的元素遍历到ObjectOutputStream
readObject方法从ObjectInputStream中读出对象并保存赋值到elementData数组