何为序列化,反序列化
将Java对象序列化为二进制形式->序列化
将二进制形式数据在内存中重建为java对象->反序列化
二进制中包含了当前实例的类的元数据,以及存储的数据等。
Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。也就是将Java对象序列化为二进制形式。
目的:
- 网络传输
- 持久化 (文件,DB等)
将序列化对象写入文件之后,可以从文件或网络中中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。
如何序列化,反序列化
类能够序列化和反序列化,
1.类必须实现Serializable接口。
2.需要序列化的属性必须实现Serializable接口(不需要的属性可以使用transient关键字修饰)
3.第一个未实现Serializable接口的父类必须有无参构造器
Java中的内置序列化机制需要使用ObjectOutputStream完成序列化。
使用ObjectInputStream完成反序列化。
ObjectOutputStream
ObjectOutputStream能够让你把对象写入到输出流中,而不需要每次写入一个字节。你可以把OutputStream包装到ObjectOutputStream中,然后就可以把对象写入到该输出流中了。代码如下:
// Serialize today's date to a file.
FileOutputStream f = new FileOutputStream("tmp");
ObjectOutput s = new ObjectOutputStream(f);
s.writeObject("Today");
s.writeObject(new Date());
s.flush();
ObjectInputStream
ObjectInputStream能够让你从输入流中读取Java对象,而不需要每次读取一个字节。你可以把InputStream包装到ObjectInputStream中,然后就可以从中读取对象了。代码如下:
// Deserialize a string and date from a file.
FileInputStream in = new FileInputStream("tmp");
ObjectInputStream s = new ObjectInputStream(in);
String today = (String)s.readObject();
Date date = (Date)s.readObject();
在这个例子中,写入和读取的顺序必须一致,数据类型也必须一致。
注意:可序列化的字段有两种方式确定
1.默认的:序列化不会处理静态和transient的属性。
2.使用serialPersistentFields
如何定制序列化的过程
1.使用默认的readObject/writeObject方法
方法签名:
private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{
ois.defaultReadObject();
//其他read代码
}
private void writeObject(ObjectOutputStream oos) throws IOException,ClassNotFoundException {
oos.defaultWriteObject();
//其他write代码
}
序列化时ObjectOutputStream调用writeObject方法,反序列化时ObjectInputStream调用readObject方法。
注意:
- 必须调用defaultWriteObject()和defaultReadObject()方法
- 写和读的顺序要一致,写了一个int,读的时候第一个也是读int.
- 序列化、反序列化过程中没有调用任何构造器(类及其父类均实现了Serializable接口时,如父类未实现Serializable,父类里的属性丢失,无法从流中反序列化)
2.使用Externalizable接口
方法签名
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
Externalizable是Serializable接口的子接口。
序列化、反序列化过程中调用了默认构造器
第一种方式可以部分定制序列化过程,序列化、反序列化的过程部分由Java控制,部分由程序员自己控制。
第二种方式则是完全定制序列化过程,程序员控制序列化、反序列化的过程.
序列化,反序列化在Java中是如何发生的
序列化:ObjectOutputStream开始向文件中写数据->反射调用writeObject方法并把自己传入->写完后转化为byte数组->写入文件
反序列化:读取文件->反射调用Object(如果某个父类没有实现Serializable接口,则调用该类的默认无参构造器(父类里的属性丢失))的构造器获取实例->设置
类型信息及其他实例化相关的元数据->初始化静态属性值->反射调用readObject方法->完成反序列化。
Externalizable 与 Serializable 的区别
Serializable | Externalizable |
标志接口,无必须实现的方法 | 必须实现writeExternal() 和 readExternal() |
JVM负责序列化实例,数据和数据类型等 | 程序员控制实例化及数据处理过程 |
默认不调用构造器 | 默认必须有无参构造器 |
修改类结构容易导致序列化失败,分析比较困难 | 分析和修改类容易 |
性能不好 | 性能受程序员实现限制,一般会比较好 |
类结构的兼容性
在序列化和反序列化的两端,java的类结构可能会有变动,有的变动会导致反序列化失败或引起业务系统的不兼容,称为非兼容的改动,
有的变动不会导致反序列化失败或业务系统的不兼容,称为兼容的改动。
注意:以下兼容性是在类实现了Serializable接口并定义了固定的serialVersionUID的情况下,如果没有实现该接口则由于serialVersionUID不同的原因,部分兼容的改动也会序列化失败。
兼容的改动
- 修改属性的修饰符:public, package, protected, private 不影响序列化
- 新增属性:新增的属性被设置默认值,可以通过readObject设置特定值。
- 将static属性改为非static,将transient属性改为非transient:类似于新增属性,被设置为默认值。
- 添加删除类:在类结构中添加的类对应的属性被设置为默认值。删除类,则类对应的属性被丢弃,但是引用到的类会被创建,在随后的垃圾回收中被回收掉。
- 某个类中添加实现Serializable接口:同添加类一样,该的属性被设置为默认值。
- 添加删除writeObject/readObject方法:添加时,方法会被调用,则可以对额外属性做序列化操作。删除时,额外添加的属性被丢弃。
不兼容的改动
- 删除属性:删除属性会导致旧版本的属性被设置为默认值,引起业务逻辑的改动。
- 类结构中提升或降低某个类的位置:不会允许,可能导致属性顺序混乱。
- 修改基础类型的属性(primitive field)的类型:由于类型不匹配导致序列化失败。引用类型会最终递归到基础类型。
- 修改writeObject/readObject方法的实现,使得某些属性不再使用默认序列化机制:即将交给jvm序列化的属性修改为直接代码处理了。类似于删除属性。writeObject/readObject对属性的处理应当保持一致。
- 将非static属性改为static,将非transient属性改为transient:类似于删除属性。
以下四种会导致数据的不兼容。 - 修改类从实现Serializable接口改为实现Externalizable接口或者相反
- 删除Serializable或Externalizable接口
- 修改枚举类型为非枚举或者相反
- 添加writeReplace或readResolve方法
需要注意的一些问题
serialVersionUID
private static final long serialVersionUID = -4116638233879810430L;
serialVersionUID是序列化的版本,用来在反序列化时,确保发送者和接收者加载的类是否兼容。其值可自定义。如果没有显示定义serialVersionUID属性,
则Java根据类定义使用SHA-1算法得出的哈希值替代。
serialVersionUID不一致时,报错InvalidCastException.
使用serialVersionUID可以在以上发生的兼容或不兼容的改动时,强制客户端更新。
transient 关键字 与 static属性
Java的序列化不考虑transient修饰的属性及static属性。
transient关键字一般用于:
- 某些属性无法被序列化
- 某些属性没有必要被序列化
- 某些属性序列化后会破坏业务逻辑,比如某个单例的属性
- 某些属性是由业务逻辑或者根据其他属性生成的
- 由于安全原因,不能序列化某些属性
父类的序列化
父类对象的序列化,需要让父类也实现Serializable 接口。
如果父类不实现,就需要有默认的无参的构造函数。
在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,
反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。
因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。
如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,
如 int 型的默认是 0,string 型的默认是 null。
readObjectNoData、writeReplace、readResolve
这三个方法都是Serializable接口的除readObject,writeObject方法之外的接口方法。
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
private void readObjectNoData()
throws ObjectStreamException;
readObjectNoData是1.4新增的,目的在于发生:
- 序列化版本不兼容
- 输入流被篡改或者损坏
时,给对象提供一个合理的值。
写入时替换对象:writeReplace
如果实现了writeReplace方法后,那么在序列化时会先调用writeReplace方法将当前对象替换成另一个对象(该方法会返回替换后的对象)并将其写入流中.
方法执行顺序:writeReplace -> writeObject/writeExternal
导致的结果:所有当前类型的实例都被替换了。假如一个类的100个不同的实例(某些属性的值完全不同)被序列化了,
反序列化时你得到了100个不同的实例,但是所有的属性可能都是一样的(取决于你的writeReplace方法的具体实现)。
读取时替换对象:readResolve
如果实现了readResolve方法后,那么在序列化时会最后调用writeReplace方法将当前对象替换成另一个对象。
方法执行顺序:readObject/readExternal -> readResolve
导致的结果:所有当前类型的实例都被替换了。
可以被用于单例模式,确保实例序列化后的唯一性。
默认实例化时,各方法的总结
功能点 | readObject | writeObject | readExternal | writeExternal | readResolve | writeReplace |
目的 | 读取定制内容 | 写入定制内容 | 读取完全定制的内容,类的实例化自己控制 | 写入完全定制的内容,写入当前对象的那些属性及写入顺序等自定义 | 覆盖读取的内容 | 覆盖写入的内容 |
是否使用默认序列化机制 | 是 | 是 | 否 | 否 | 是,但readObject/readExternal读到的内容被丢弃 | 是,但写入流的原类的实例都被丢弃 |
调用顺序 | readObject -> readResolve | writeReplace -> writeObject | readExternal -> readResolve | writeReplace -> writeExternal | readObject/readExternal -> readResolve | writeReplace -> writeObject/writeExternal |
是否成对 | 是,必须与writeObject一起添加 | 是 | 是必须与writeExternal一起添加 | 否,可独立存在 | 否,可独立存在 |
各个方法的目的不同,理解了目的,其他就好理解了。
序列化,反序列化与深克隆(deep clone)
java的克隆是属性到属性的克隆。
- 如果属性是基础数据类型,新的属性对象会被创建。
- 如果属性是引用类型,则只有引用被复制,引用指向的对象是同一个。意味着克隆对象的改动会影响被克隆对象。
java实现克隆需要两步。
- 实现clonable接口
- override Object对象的clone方法
java默认的克隆方式的浅克隆:即只克隆基础数据类型,引用类型只复制了引用。
深克隆:克隆所有的数据,使得对克隆对象的任何操作不会影响到原被克隆对象。
深克隆的方式可使用以下几种方式:
- 使用该类型参数构造器
public class Data {
private Integer x;
private Object y;
public Data(Data point){
this.x = point.x;
this.y = point.y;
}
}
- 使用序列化、反序列化
public static <T> T deepCopy(T Object) throws IOException, ClassNotFoundException {
//Serialization of object
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
System.out.println("begin w");
out.writeObject(Object);
System.out.println("end w");
//De-serialization of object
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream in = new ObjectInputStream(bis);
T copied = (T) in.readObject();
System.out.println("end r");
return copied;
}
使用ByteArrayInputStream和ByteArrayOutputStream在内存中完成克隆
- 使用Apache commons工具
SomeObject cloned = org.apache.commons.lang.SerializationUtils.clone(someObject);
以上三种方式的性能未测试。
反序列化破坏单例
反序列化默认没有调用构造器就实例化了类。对单例的类无法阻止生成多个实例。破坏了单例。
解决方案就是使用保护性反序列化方法readResolve
protected Object readResolve(){
return getInstance();
}
序列化中的异常
异常 | 原因 |
InvalidClassException | 以下原因导致的无法还原实例时,1.serialVersionUID 不一致 2.同一属性的基础类型改变时 3.最近的未序列化的父类没有无参构造器 4.Externalizable 的类没有无参构造器 |
StreamCorruptedException、InvalidObjectException | 一般是数据损坏,或协议不匹配 |
OptionalDataException | 属性本来是基础类型,被改成了引用类型 |
建议及最佳实践
- 一定要加serialVersionUID
- readObject,writeObject最好同时存在,读写数据时保持顺序一致
- readObject,writeObject必须调用默认的序列化方法
- 可序列化的对象添加无参构造器
- 了解transient关键字的常用功能