本文将首先介绍序列化的用途,然后介绍序列化的基本用法,继而提出在序列化使用中需要注意的一些规范;如果有进一步兴趣,可以继续查看序列化的原理,加深理解。
1.序列化用途
所谓的序列化,就是把对象转成对应的字节数组;反序列化,即把字节数组转成其关联的对象。
这些字节序列中保存的信息其实是这个对象的状态信息,即它的成员变量。
大家知道,计算机存储或者通讯,主要是通过字节流来进行。因此,通过序列化,我们可以把对象进行本地持久化,或者进行网络传输,例如rmi、rpc机制中对象的传输。
2.如何序列化
我们首先构建一个本次需要进行序列化的类
public class PersonUsingSerializable implements Serializable {
private static final long serialVersionUID =1L;
private String name;
private int age;
private LocalDatebirthday;
/**
* 字段不需要持久化
*/
transient private int seq;
public String get Name() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public LocalDate getBirthday() {
return birthday;
}
public void setBirthday(LocalDate birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return String.format("Person, name:[%s], age:[%d], birthday:[%s], seq:[%d]",this.getName(), this.getAge(), this.getBirthday(), this.getSeq());
}
public int getSeq() {
return seq;
}
public void setSeq(int seq) {
this.seq = seq;
}
}
2.1 基本操作:实现java.io.Serializable接口
对于比较简单的类(类似一个单纯且无复杂数据结构的类),像上述PersonUsingSerializable类,我们仅需实现一个接口即可。然后利用java.io.ObjectOutputStream/java.io.ObjectInputStream,完成相关的字节流输出/输入。
public class SerializableDemo {
public static void main(String[] args) throws Exception{
PersonUsingSerializable person =new PersonUsingSerializable();
person.setName("peter");
person.setAge(18);
person.setBirthday(LocalDate.of(1989, 1, 1));
person.setSeq(10);
final String filePath ="E://tmp/person.txt";
System.out.println("persisting person...");
ObjectOutputStream oos =new ObjectOutputStream(new FileOutputStream(filePath));
oos.writeObject(person);
System.out.println("restore person...");
ObjectInputStream ois =new ObjectInputStream(new FileInputStream(filePath));
Object restorePerson = ois.readObject();
System.out.println("profile of this object:" + restorePerson);
}
}
可以看到输出结果,对象能正常序列化及反序列化,注意其中seq字段并未被持久化:
persisting person...
restore person...
profile of this object:Person, name:[peter], age:[18], birthday:[1989-01-01], seq:[0]
默认序列化机制中,只会序列化非static和非transient字段。
2.2 定制操作
如果不满足于默认的java序列化机制,jdk提供了如下接口:
- private void writeObject(java.io.ObjectOutputStream out) throws IOException;
针对特定的类字段的状态保存,可以使用该方法。其它默认字段的保存可以通过ObjectOutputStream#defaultWriteObject()方法触发(值得一提,对于实现了这个方法的类,可以不关心它的子类或超类的相关字段的保存)。特定类的字段保存,可以通过调用ObjectOutputStream的writeObject方法或对应的一些原始类型的写入方法。
- private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException;
这个方法和writeObject相对应。针对子类或超类、其它一些默认字段,通过InpupStream的defaultReadObject方法,触发默认的反序列化机制。需要注意一点,需要单独操作的读字段的顺序要和对象的写字段顺序一致。
- private void readObjectNoData() throws ObjectStreamException;
如果反序列化类和序列化类,在继承体系上不兼容(比如:反序列化的类继承的类和序列化的类继承的类的版本不一致或者序列化流被破坏,数据有损),这个时候该方法就可以排上用场。
- ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
当需要替换写入对象时,可以使用该方法。
- ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
如果需要替换读入的反序列化对象时,可以使用该方法。该方法常用在单例模式中。
- 相关代码示例:
- writeObject()和readObject()
public class PersonUsingSerializable implements Serializable {
// ......
private void writeObject(ObjectOutputStream out)throws IOException {
out.defaultWriteObject();
out.writeObject(this.getName() +" from serialize");
}
private void readObject(ObjectInputStream in)throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.name = (String)in.readObject();
this.seq =10;
}
}
通过输出结果,可以看出,name的输出被替换了,可以体现在反序列化中;而之前因为transient的原因未序列化的seq字段,因为readObject()中的相关操作,也获得了初始值。
persisting person...
restore person...
profile of this object:Person, name:[peter from serialize], age:[18], birthday:[1989-01-01], seq:[10]
-
writeReplace()和readResolve()
感觉应用场景不多。1)在单例模式中,为了序列化和反序列化的对象仍是唯一的单例对象。可以在方法中返回这个单例对象,以免单例模式失败;2)还有一种就是effective java中提倡的:考虑使用序列化代理代替序列化实例。
2.3 彻底掌控序列化:实现java.io.Externalizable
通过实现该接口,对应的类可以完全控制序列化流的格式和内容。实际上,Externalizable接口实现了Serializable接口,java序列化机制会先根据serializable接口,判断对象是否可以序列化;进一步,根据是否有实现了Externalizable接口,判断序列化方式。
示例如下:
public class PersonUsingExternalizable implements Externalizable {
private static final long serialVersionUID =1L;
private Stringname;
private int age;
private LocalDatebirthday;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public LocalDate getBirthday() {
return birthday;
}
public void setBirthday(LocalDate birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return String.format("Person, name:[%s], age:[%d], birthday:[%s]", this.getName(), this.getAge(), this.getBirthday());
}
@Override
public void writeExternal(ObjectOutput out)throws IOException {
out.writeObject(this.name);
out.writeInt(this.age);
out.writeObject(this.birthday);
}
@Override
public void readExternal(ObjectInput in)throws IOException, ClassNotFoundException {
this.name = (String)in.readObject();
this.age = in.readInt();
this.birthday = (LocalDate)in.readObject();
}
}
3.序列化相关规范或注意事项
可以参考Effective Java中关于序列化操作(第11章)的相关注意事项,这里简单总结如下:
- 谨慎地实现Serializable接口
在发布一个接口的同时,如果发布的类支持序列化,那么这个类的字节编码也就成为发布的接口的一部分。特别地,如果使用默认的序列化形式,这个类中的私有和包级私有的实例域都将变成导出的API的一部分,这不符合“最低限度地访问域”的实践准则。
- 考虑使用自定义的序列化形式
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。否则,“即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性。”
- 保护性地编写readObject方法
readObject方法实际上相当于另一个共有的构造器,如同其它的构造器一样,它也要求同样的所有注意事项。构造器必须检查其参数的有效性,并且在必要的时候对参数进行保护性拷贝。
- 考虑使用序列化代理代替序列化实例
4.序列化原理
接下来将介绍如下代码的实现机制
// 1.序列化,简单介绍对象到输出流的基本过程
ObjectOutputStream oos =new ObjectOutputStream(new FileOutputStream(filePath));
oos.writeObject(person);
// 2.反序列化,请参考序列化,不赘述
ObjectInputStream ois =new ObjectInputStream(new FileInputStream(filePath));
Object restorePerson = ois.readObject();
4.1 序列化
分两步。
(1) new ObjectOutputStream():会首先写入序列化的魔法数和版本,以及相关配置的初始化
public ObjectOutputStream(OutputStream out)throws IOException {
verifySubclass();
/**
* 内部是使用块输出流
**/
bout =new BlockDataOutputStream(out);
enableOverride =false;
// ......
// 设置文件头
writeStreamHeader();
bout.setBlockDataMode(true);
}
(2) ObjectOutputStream#writeObject()方法核心流程
首先我们需要知道,在该核心方法中,需要关注的三个类:
- java.io.ObjectOutputStream.BlockDataOutputStream
即,ObjectOutputStream方法内部输出流的操作对象
- java.io.ObjectStreamClass
Serialization's descriptor for classes. It contains the name and serialVersionUID of the class.
- java.io.ObjectStreamField
A description of a Serializable field from a Serializable class.
- 接下来,描述下整体关键流程:
a.根据传入的对象的所属类型,获取类序列化描述符ObjectStreamClass(java.io.ObjectStreamClass#lookup);
b.判断序列化对象的类型:String\Array\Enum\Serializable,如果实现了Serializable接口,则走步骤c后的逻辑;
c.根据Externalizable和Serializable接口,走不同的序列化逻辑;
d.如果是externalizable接口,不使用ObjectStreamClass对象,调用传入对象的writeExternal方法写入数据,结束;
e.如果是Serializable接口,根据类是否有writeObject()方法,判断对应逻辑。
f.对于默认的Serializable机制,会通过ObjectStreamClass获取序列化对象的需要序列化的字段,然后针对需要序列化的字段,逐个递归执行序列化操作(步骤b)
附上截取的代码详解:
- 核心流程(按代码逻辑放置):
// 该方法是对象序列化的入口方法,是整个对象序列化的入口;另外,对象内部的字段也会递归调用该方法进行各自的序列化
private void writeObject0(Object obj, boolean unshared) throws IOException
{
//......
ObjectStreamClass desc;
for (;;) {
Class repCl;
// 获取序列化对象的描述符
desc = ObjectStreamClass.lookup(cl, true);
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) ==null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl;
}
//......
// 根据对象类型,判断如何执行输出流,可以看出,如果普通对象没有实现Serializable接口,是没办法进入序列化流程的
if (obj instanceof String) {
writeString((String) obj, unshared);
}else if (cl.isArray()) {
writeArray(obj, desc, unshared);
}else if (ob jinstanceof Enum) {
writeEnum((Enum) obj, desc, unshared);
}else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
}else {
if (extendedDebugInfo) {
throw new NotSerializableException(cl.getName() +"\n" +debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
- java.io.ObjectStreamClass#lookup方法
该方法内部会查找序列化类是否有对应的描述符,如果没有,会内部new一个ObjectStreamClass,并且进行缓存。需要注意,针对多线程竞争的境况,这里面采用了自旋锁的方式,首先创建一个EntryFuture;成功创建EntryFuture的线程可以进行描述符的创建,其它线程则进入wait操作。
static ObjectStreamClasslookup(Class cl, boolean all) {
if (!(all || Serializable.class.isAssignableFrom(cl))) {
return null;
}
//通过自旋方式,获得有效的EntryFuture
//-----------------------------------------------------
processQueue(Caches.localDescsQueue, Caches.localDescs);
WeakClassKey key =new WeakClassKey(cl, Caches.localDescsQueue);
Reference ref = Caches.localDescs.get(key);
Object entry =null;
if (ref !=null) {
entry = ref.get();
}
EntryFuture future =null;
if (entry ==null) {
EntryFuture newEntry =new EntryFuture();
Reference newRef =new SoftReference<>(newEntry);
do {
if (ref !=null) {
// 因为该ref不含有有效的Entry,估删除对应的Ref
Caches.localDescs.remove(key, ref);
}
// 尝试放入自己创建的有效的Ref引用;这时候,利用ConcurrentHashMap,保证只有一个插入成功
ref = Caches.localDescs.putIfAbsent(key, newRef);
// 判断是否能从插入返回的引用中获取有效的entry,如果没有,会继续本轮循环
if (ref !=null) {
entry = ref.get();
}
}while (ref !=null && entry ==null);
if (entry ==null) {
future = newEntry;
}
}
if (entry instanceof ObjectStreamClass) {// check common case first
return (ObjectStreamClass) entry;
}
// 判断EntryFuture的创建线程,如果是其它线程创建,则阻塞等待Future返回描述符即可;否则,置null,以走后续创建逻辑
// ---------------------------------------------
if (entry instanceof EntryFuture) {
future = (EntryFuture) entry;
if (future.getOwner() == Thread.currentThread()) {
entry =null;
} else {
entry = future.get();
}
}
// 当前线程赢得创建类描述符的权利,开始创建类描述符了
//------------------------------------------------------------
if (entry ==null) {
try {
// 调用构造方法进行初始化
entry =new ObjectStreamClass(cl);
} catch (Throwable th) {
entry = th;
}
if (future.set(entry)) {
Caches.localDescs.put(key, new SoftReference(entry));
} else {
// nested lookup call already set future
entry = future.get();
}
}
if (entry instanceof ObjectStreamClass) {
return (ObjectStreamClass) entry;
} else if (entry instanceof RuntimeException) {
throw (RuntimeException) entry;
} else if (entry instanceof Error) {
throw (Error) entry;
} else {
throw new InternalError("unexpected entry: " + entry);
}
}
- ObjectStreamClass的构造方法会通过反射,获取序列化类的各种描述信息,包括是否实现serializable接口、是否实现externalizable接口,是否是枚举或代理类,解析可序列化的字段,解析获取私有的writeObject、readObject等方法。
private ObjectStreamClass(final Class cl) {
//......
isProxy = Proxy.isProxyClass(cl);
isEnum = Enum.class.isAssignableFrom(cl);
serializable = Serializable.class.isAssignableFrom(cl);
externalizable = Externalizable.class.isAssignableFrom(cl);
//......
fields =getSerialFields(cl);
//......
if (externalizable) {
cons =getExternalizableConstructor(cl);
} else {
cons =getSerializableConstructor(cl);
writeObjectMethod =getPrivateMethod(cl, "writeObject",
new Class[] { ObjectOutputStream.class },
Void.TYPE);
readObjectMethod =getPrivateMethod(cl, "readObject",
new Class[] { ObjectInputStream.class },
Void.TYPE);
readObjectNoDataMethod =getPrivateMethod(cl, "readObjectNoData", null, Void.TYPE);
hasWriteObjectData = (writeObjectMethod !=null);
}
writeReplaceMethod =getInheritableMethod(cl, "writeReplace", null, Object.class);
readResolveMethod =getInheritableMethod(cl, "readResolve", null, Object.class);
// ......
}
- 一切就绪,开始序列化了,看看writeOrdinaryObject方法
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
if (extendedDebugInfo) {
debugInfoStack.push((depth ==1 ?"root " :"") +"object (class \"" +obj.getClass().getName() +"\", " + obj.toString() +")");
}
try {
desc.checkSerialize();
bout.writeByte(TC_OBJECT);
writeClassDesc(desc, false);
handles.assign(unshared ?null : obj);
if (desc.isExternalizable() && !desc.isProxy()) {
// 执行实现了Externalizable的类的序列化
// 该方法内部实际上就是直接调用方法的writeExternal方法,然后结束序列化操作
writeExternalData((Externalizable) obj);
} else {
// 执行实现了Serializable的类的序列化
writeSerialData(obj, desc);
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
- 对于使用默认序列化的类,writeSerialData方法内部,会调用defaultWriteFields方法,这个方法会循环遍历描述符中的每一个需要序列化的字段,然后调用writeObject0方法,走对象序列化流程。
private void defaultWriteFields(Object obj, ObjectStreamClass desc)
throws IOException
{
Class cl = desc.forClass();
if (cl !=null && obj !=null && !cl.isInstance(obj)) {
throw new ClassCastException();
}
desc.checkDefaultSerialize();
int primDataSize = desc.getPrimDataSize();
if (primVals ==null ||primVals.length < primDataSize) {
primVals =new byte[primDataSize];
}
desc.getPrimFieldValues(obj, primVals);
bout.write(primVals, 0, primDataSize, false);
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals =new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
desc.getObjFieldValues(obj, objVals);
for (int i =0; i < objVals.length; i++) {
if (extendedDebugInfo) {
debugInfoStack.push(
"field (class \"" + desc.getName() +"\", name: \"" + fields[numPrimFields + i].getName() +"\", type: \"" +fields[numPrimFields + i].getType() +"\")");
}
try {
// 又走到序列化对象的方法啦
writeObject0(objVals[i],fields[numPrimFields + i].isUnshared());
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
}
4.2 反序列化
可以参照序列化方法自个分析下。
5.其它序列化框架
除了java原生自带的序列化框架,出于压缩速度、压缩比率的角度,一般可以使用其它的序列化框架。这里搜集了一些其它序列化框架进行比较。