什么是序列化和反序列化:
序列化就是冻结对象。java的对象随着JVM的运行而被保持在内存中,随着JVM的停止而丢弃消亡。很多时候,这些对象或是不可重建的,或是重建对象将付出巨大的代价,JVM运行时,少量的对象被保持在内存中是可以接受的。然而一旦JVM需要被停止,或在运行过程中建立了过多的对象,对象的数量多到影响操作系统的正常运行乃至多到物理内存都存不下时,这些对象只能想办法保存起来。
提到保存对象,或者说是持久化。最熟知的莫过于保存到文件系统或数据库。这种做法一般涉及到自定义存储格式以及繁琐的数据转换。Java序列化就是java提供的非常简单易用的对象保存为字节数组的方法。除此以外,使用RMI(远程方法调用),或用网络传递对象时,都会用到对象序列化。
反序列化就是从字节码到对象的过程。
什么是可序列化的:
我们看一下ObjectOutputStream的源码,可以知道,ObjectOutputStream对象可以序列化所有的基础数据类型和
支持 java.io.Serializable 接口的对象,值得一提的是:各种类型的数组、枚举、字符串都支持java.io.Serializable 接口。
默认的序列化机制:
Java提供了良好的默认支持,实现默认的对象序列化是非常简单的。只需要让需要序列化的对象实现Serializable 接口,就可以用ObjectOuputStream和ObjectInputStream对对象序列化和反序列化。Serializable 的作用只是起到标记该类可以序列化,不用实现任何方法,就能把对象的各种属性、对象引用的各种对象全部序列化。
默认的序列化范围:
默认序列化机制会试图序列化对象的所有内部属性,并序列化支持序列化的超类(如果有)的所有内部属性。但是要注意,静态属性忽略、用瞬态修饰符transient修饰的属性也会忽略。要序列化的对象、对象引用的对象、对象引用的引用等等所有相关的对象,这些对象都会被序列化,都必须支持Serializable 接口。
序列化对类的处理原则:
一个类如果支持序列化,则其子类都支持序列化,其超类不一定支持序列化。序列化一个对象时,支持序列化的超类会像子类一样序列化--不调用构造函数,仅凭序列化字节码就可以生成这个对象。
对象如果有不支持序列化的超类,则这些超类不会被序列化,但会调用这些超类中继承层次最低的超类的构造代码且调用其中的无参构造函数构造默认的父对象。如果没有无参构造函数,抛出InvalidClassException异常。最终生成的对象,支持序列化的超类中定义的属性值由序列化输入流输入的字节决定,不支持序列化的超类中定义的属性值构造生成。
不支持序列化的超类中定义的属性值由构造代码决定,不是由序列化决定。如果没有在构造代码中设置,那么属性的值应当是默认值,即各种数值型属性为0或0.0,boolean 为false,对象是null。因此序列化前对象属性的值可能与反序列化后对象属性的值不同,需要警惕。
构造对象时代码的执行顺序为——超类的静态代码块或静态赋值语句(按出现顺序)(仅在该类加载时执行一次)——本类的静态代码块或静态赋值语句——超类的构造代码块或构造赋值语句(按出现顺序)——本类的构造代码块或构造赋值语句——超类的构造函数——本类的构造函数。
static{
//静态代码块
}
static int i = 99;//静态赋值语句
{
//构造代码块
}
int i = 3;//构造赋值语句
public xx(){
//构造函数
}
限定序列化的数据:
很多时候用默认机制序列化对象都是不合理的。或者对象的某个属性引用的对象不能支持序列化接口,或者对象中包含了一些敏感的数据如银行卡的账号和密码,或者对象包含的数据并非都是有意义的比如临时变量。这时,我们可以指定那些数据可以序列化,那些是不需要的。
限定序列化的数据的方法有两种,一种是用瞬态修饰符transient修饰不用序列化的属性。另外一种是添加一个serialPersistentFields域来声明序列化时要包含的域。
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("firstName", String.class) //声明String类型的域名为firstName的域要被序列化的
};
private void writeObject(ObjectOutputStream output) throws IOException {
//序列化前数据的加密。。。。
//把数据二进制化为流可以调用默认的序列化机制(不是必须的)
output.defaultWriteObject();
//也可以手工把数据二进制化
output.writeUTF("Hello World");
}
private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
//可以调用默认的反序列化机制
input.defaultReadObject();
//也可以手工的反序列化
String this.value = input.readUTF();
//序列化后的数据解密
//对象初始化
//对象有效性检查
}
如上面的源码所示,类如果实现了private void writeObject(ObjectOutputStream output),函数,序列化时就会自动调用writeObject函数序列化对象,不再执行默认的序列化机制,writeObject中调用的output.defaultWriteObject()是序列化默认机制的实现函数,可以说序列化的默认机制就是writeObject简单的调用defaultWriteObject()函数实现的。此外,ObjectOutputStream还提供了丰富的向序列化流写入数据的方法,例如output.writeUTF("Hello World")手动的写入了一个字符串。
与之对应的,private void readObject(ObjectInputStream input)在反序列化时被调用,使默认机制失效。input.defaultReadObject()是反序列化的默认机制的实现函数。同样的ObjectInputStream提供了丰富的读取数据的方法,例如input.readUTF()读取了一个字符串。
注意:readObject函数刚开始运行时但还没有做任何操作时,this对象没有初始化,this所属的类定义的属性的值都是默认值,直到在readObject中对其赋值才会改变。当然,此时父对象已经通过序列化机制或父类的构造函数初始化,父类的属性可能已经赋值。
writeObject和readObject除了可定制序列化实现,还可以用于对关键敏感数据的加解密、可能的恶意伪造的序列化流反序列化后的对象的有效性的检查。以及对象在反序列化后初始化、使与计算机紧密关联的资源如线程、IO资源、本地资源、网络资源等数据重新有效,为没有被序列化的属性重新赋值。
序列化存储规则:
Java 序列化机制为了节省磁盘空间,具有特定的存储规则,如果写入序列化流的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,反序列化时,恢复引用关系。最终反序列得到的两个对象的引用指向同一个对象,且为第一次写入的对象。
解决方法:ObjectOutputStream.writeUnshared(Object obj)写入的对象总是作为新对象被写入流中,对象总是独享唯一的,不管该对象以前是否已经被写入过,即使再写入同一个对象,只会重新将对象的内容进行存储,却不会储存为前一个对象的引用。
危险:被恶意伪造的序列化流可以在流中轻易地添加对某个对象的引用(在序列化流中,对象的引用只是一个简单的对象的标号,没有对象的具体状态,因此伪造写入了一个对象,且这个对象指向伪造前的序列化流中的对象,这是非常简单的),从而获得这个对象的引用用于读写对象的内部状态,这对程序的安全性是一个极大的威胁。解决方法:创建一个新的有相同内容的对象。
序列化一个单例对象:
事实上,序列化机制创建的对象,其实质是根据对象的class文件,反射创建一个所有属性的为默认值的对象,再根据序列化流中的数据,填充到对象中。这个新对象是且只是看起来像原来的对象,与原来的对象没有更多的关系了,如果对象存储的是与系统硬件密切相关的数据,往往还要重新初始化这些数据,这些数据才是有效的。
我们可能希望使用另外一个指定的对象来代替当前对象序列化,可能希望用指定的对象替换反序列化得到的对象。原因可能是当前对象中包含了一些不希望被序列化的域;也可能是希望隐藏实际的类层次结构;还有可能是为了保证某个类在JVM中只有一个实例。
实现:
ANY-ACCESS-MODIFIER Object readResolve()throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object writeReplace()throws ObjectStreamException;
writeReplace在序列化即将开始时调用,调用返回的对象会被序列化(当前对象被忽略),即ObjectOutputStream.readObject(readResolve())。readResolve在序列化完成后返回前调用,返回的对象替换原来要返回的对象,ObjectInputStream.readObject()返回writeReplace的结果。
某个包含敏感数据的源对象序列化时writeReplace把不包含敏感数据却又可以重建源对象的替换对象写入到流中,反序列化时readResolve用从流读取的替换对象重建为源对象。这样对调用者来说,替换对象的存在就是透明的。却又可以有效避免源对象中的敏感数据被公开传输。
序列化的版本管理:
序列化机制通过Serializablee接口声明可以序列化的类,通过全局惟一标识符serialVersionUID声明某类的惟一的序列化版本号。定义如下:
private static final long serialVersionUID = 1L
这个属性隐含在所有支持Serializablee接口的类中,如果一个类显示声明了该属性,则此类的版本号就是属性的值,如果没有声明该属性,JDK综合Java类的各个特性计算出来一个哈希值作为版本号。序列化时serialVersionUID被写入到流中,反序列化机制从流中读取对象所属类的版本号,与JVM存储的类的版本号相比较,不同则认为两个类是不兼容的。
对于开发者来说,想要保持版本的兼容只需要在支持Serializable接口的类中定义这样的一个域,并在后续版本更新过程中保持该值不变。一般而言,在新的版本中添加些域不会有兼容问题,掉一些域则是不行的。在不想维持这种兼容性时,换个版本号就好了。
Externalizable接口的使用:
Externalizable是Serializable的子接口,区别在于Serializable有默认的实现机制,只要支持了接口,就能以默认方式序列化。Externalizable则必须实现两方法:
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
序列化支持
Externalizable接口的对象时,
writeExternal方法会被序列化机制调用,可以在方法内手工的调用
ObjectOutput提供的方法向流中写入数据。反序列化支持Externalizable接口的对象时,先用对象所属类的无参构造函数生成一个默认的对象,然后调用readExternal方法从流中读取数据填充对象。
Externalizable与Serializable的区别主要在于,Externalizable会调用对象所属类的无参构造函数,然后必须手工设置对象的属性。Serializable自动的读取对象的属性并设置为对象属性的值,却不用构造函数,只有不支持Serializable才会用构造函数构造。
Externalizable:构造对象,手工对属性赋值,底层资源易重用。
Serializable:复制对象,自动赋值,简单代码少。
readObjectNoData方法:
如果序列化流没有将给定类列为要反序列化的对象的超类,则 readObjectNoData 方法负责初始化其特定类的对象状态。在接收方使用的反序列化实例类的版本不同于发送方,并且接收者版本扩展的类不是发送者版本扩展的类时,此事可能发生。如果序列化流已经被篡改,也会发生这种情况;因此,不管源流是“敌意的”还是不完整的,readObjectNoData 方法都可以用来正确地初始化反序列化的对象。
序列化安全性:
Java对象序列化之后的内容格式是公开的。所以可以很容易的从中提取出各种信息。从实现的角度来说,可以从不同的层次来加强序列化的安全性。
对序列化之后的流进行加密。这可以通过CipherOutputStream来实现。
实现自己的writeObject和readObject方法,在调用defaultWriteObject前,先对要序列化的域的值进行加密处理。
使用一个SignedObject或SealedObject来封装当前对象,用SignedObject或SealedObject进行序列化。
在从流中进行反序列化的时候,可以通过ObjectInputStream的registerValidation方法添加ObjectInputValidation接口的实现,用来验证反序列化之后得到的对象是否合法。
序列化框架:
优秀的序列化框架有很多例如kryo、hessian、java、protostuff。
引用一个序列化框架的比较:http://www.oschina.net/question/54100_91827