序列化的概念与目的
由于在系统底层,数据的传输形式是简单的字节序列,在 Java 中的表现就是 byte[] 二进制数据,即 在底层,系统不认识对象,只认识字节序列,而为了达到进程通讯的目的,需要先将数据序列化,而 序列化就是将对象转化为字节序列的过程;相反的,当字节序列被运到相应进程的时候,进程 为了识别这些数据,就要将其反序列化,即把字节序列转化为对象。
无论是在进程间通信,本地数据存储或者是网络数据传输都离不开序列化的支持。而针对不同场景选择合适的序列化方案对于应用的性能有着极大的影响。
数据序列化相当于是将我们原先的对象序列化概念做出了扩展,在对象序列化和反序列化中,我们熟知的有两种方式,Java 提供了 Serializable 接口,Android 提供了 Parcelable 接口。在这里我们对序列化这个概念做出了扩展,例如 json、xml、sqlite 等数据格式和本地存储方案,从广义上来说,这些都可以算做是数据的序列化。
总结下序列化的概念与目的:
序列化协议与特性
一种协议或实现方式能被作为合适的序列化方案,它需要有以下的特性:
-
通用性:
-
强健性/鲁棒性:
-
可调试性/可读性:
-
性能:性能包括两个方面,时间复杂度和空间复杂度
-
可扩展性/兼容性:序列化协议需要具有良好的扩展性,增加新的业务字段的同时不影响老的服务,这将大大提高系统的灵活度 -
安全性/访问限制:在序列化选型过程中,安全性的考虑往往发生在跨局域网访问的场景。当通讯发生在公司之间或者跨机房的时候,处于安全考虑,对于跨局域网的访问往往被限制为基于 HTTP/HTTPS 的 80 和 443 端口。如果使用的序列化协议没有兼容成熟的 HTTP 传输层框架支持,可能会导致以下三种结果之一:
-
因为访问限制而降低服务可用性 -
被迫重新实现安全协议而导致实施成本大大提高 -
开放更多的防火墙端口和协议访问,而牺牲安全性
常见的序列化协议说明
XML&SOAP(Simple Object Access Protocol)
XML 是一种常用的序列化和反序列化协议,具有跨机器、跨语言等优点,SOAP 是一种被广泛应用的、基于 XML 为序列化和反序列化协议的结构化信息传递协议。
JSON(Javascript Object Notation)
JSON 起源于弱类型语言 Javascript,它的产生来自于一种称之为 Associative array 的概念,其本质是采用 Attribute-value 的方式来描述对象。实际上在 Javascript 和 PHP 等弱类型语言中,类的描述方式就是 Associative array。JSON 的如下优点,使得它快速成为最广泛使用的序列化协议之一:
-
Associative array 格式非常符合工程师对对象的理解 -
保持了 XML 的 Human-readable 的优点 -
相对于 XML 而言,序列化后的数据更加简洁。有研究表明 XML 所产生序列化之后文件的大小接近 JSON 的两倍 -
具备 Javascript 的先天性支持,所以被广泛应用于 Web browser 的应用场景中,是 Ajax 的事实标准协议 -
与 XML 相比,其协议比较简单,解析速度比较快 -
松散的 Associative array 使得其具有良好的可扩展性和兼容性
Protobuf
Protobuf 具备了优秀的序列化协议所需的众多典型特性:
-
标准的 IDL 和 IDL 编译器,这使得其对工程师非常友好 -
序列化数据非常简洁、紧凑,与 XML 相比,其序列化之后的数据量约为 1/3 到 1/10 -
解析速度非常快,比对应的 XML 快约 20-100 倍 -
提供了非常友好的动态库,使用非常简洁,反序列化只需要一行代码
Serializable
Serializable 是 Java 提供的序列化接口,它是一个空接口:
public interface Serializable {
}
实际做序列化和反序列化的是 ObjectOutputStream 和 ObjectInputStream。ObjectoutputStream 是实现了序列化逻辑的序列化工具,它可以把一个对象拆分为类、方法、字段、数据信息并将它们转换为字节数据;ObjectInputStream 是反序列化工具,它可以将拆完的字节数据重新组装恢复并重新构建一个对象出来。
Serializable 的作用是标识当前类可以被 ObjectOutputStream 序列化和被 ObjectInputStream 反序列化。
serialVersionUID
当我们让一个类实现 Serializable 接口后,默认情况下 IDE 不会有提醒添加 serialVersionUID。那 serialVersionUID 的作用是什么?我们通过一个 demo 讲解它的作用。
public class Person implements Serializable {
private int sex = 1;
private int age = 27;
}
private static void writeObject() {
try (FileOutputStream fos = new FileOutputStream("/Volumes/MacintoshData/develop/test.out");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
Person person = new Person();
oos.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
}
private static void readObject() {
try (FileInputStream fis = new FileInputStream("/Volumes/MacintoshData/develop/test.out");
ObjectInputStream ois = new ObjectInputStream(fis)) {
Person person = (Person) ois.readObject();
System.out.println(person);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
java.io.InvalidClassException: com.example.lib.Person; local class incompatible: stream classdesc serialVersionUID = 5619280107360563824, local class serialVersionUID = -53112556261829861
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.example.lib.Test.readObject(Test.java:31)
at com.example.lib.Test.main(Test.java:13)
上面的代码非常简单,定义了一个 Person 类,然后先执行 writeObject() 将对象序列化存储到一个文件,在反序列化前将 Person 类成员变量 name 注释掉,此时反序列化就会抛出异常,提示 serialVersionUID 不一致。
serialiVersionUID 是用来做版本管理的。jdk 官方在考虑拆解拼装(序列化和反序列化)数据的时候考虑到有数据变动的问题,所以用一个变量作为标识。如果我们自己不写,JVM 会根据类的相关信息计算数值,自己写则自己控制版本,数值自定义。
为了做好兼容性,JVM 规范强烈建议我们手动声明 serialVersionUID,同时最好是 private 和 final 的尽量保持不变,这样,即使某个类在序列化后被修改,该对象依然可以被正确反序列化;否则 serialVersionUID 由 JVM 根据类信息计算出数值,修改后的类的数值往往不同,最终导致反序列化失败。
不手动指定 serialVersionUID 的另一个坏处是不利于程序在不同的 JVM 之间移植,因为不同的编译器实现计算的数值可能不同,从而造成类虽然没有改变但无法兼容导致反序列化失败。
上面的 demo 将 serialVersionUID 手动加上后,在序列化时将类中的成员都序列化,反序列化时也能正常序列化,但是成员变量 name 的值为 null。
成员变量是类,没有实现 Serializable,是否可以序列化?
如果成员变量也是一个类,那么该成员变量的类也需要实现 Serializable 接口,否则无法序列化会抛出 NotSerializableException:
public class Person implements Serializable {
private static final long serialVersionUID = 1;
private String name = "demo";
private int sex = 1;
private int age = 27;
private Address address = new Address();;
}
public class Address {
private String street = "street";
}
java.io.NotSerializableException: com.example.lib.Address
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.example.lib.Test.writeObject(Test.java:20)
at com.example.lib.Test.main(Test.java:12)
为了能正常序列化,成员变量也要实现 Serializable:
public class Person implements Serializable {
private static final long serialVersionUID = 1;
private String name = "demo";
private int sex = 1;
private int age = 27;
private Address address = new Address();
}
public class Address implements Serializable {
private String street = "street";
}
一次序列化的过程,本质上来讲是一次深克隆,即会将数据复制一份,因为 Person 类要做序列化要做数据保存,内部的成员类也要连带进行序列化。
子类没有实现 Serializable,父类实现了,是否可以序列化?
如果子类没有实现 Serializable,父类实现了 Serializable 接口,是否可以正常序列化和反序列化呢?
public class Student extends Person {
private String classmate = "A";
@Override
public String toString() {
return super.toString() + "\nStudent{" +
"classmate='" + classmate + '\'' +
'}';
}
}
输出:
Person{name='demo', sex=0, age=27, address=Address{street='test'}}
Student{classmate='A'}
可以发现即使子类没有实现 Serializable,序列化和反序列化过程都是正常的,也就是说,Serializable 是有继承性的。
反过来子类实现了 Serializable,父类没有实现,也是可以正常序列化和反序列化。
transient
在一个类中我们可能会有这样一种需求:不希望将类中所有的字段都序列化,根据需要只序列化一些需要的字段,或排除一些不需要序列化的字段。这时候就需要用到 transient 关键字。
通过 transient 声明的成员变量将不参与序列化,在反序列化时该成员变量会被设置为初始值。
public class Person implements Serializable {
private static final long serialVersionUID = 1;
private String name = "demo";
private transient int sex = 1;
private int age 27;
private Address address = new Address();
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
", address=" + address +
'}';
}
}
public class Address implements Serializable {
private static final long serialVersionUID = 2;
private String street = "test";
@Override
public String toString() {
return "Address{" +
"street='" + street + '\'' +
'}';
}
}
输出:反序列化成员变量 sex 是初始值 0,如果是对象类则为 null
Person{name='vincent', sex='0', age=27, address=Address{street='test'}}
除了使用 transient 声明的成员变量不参与序列化外,静态成员变量也不会参与序列化,因为静态成员变量属于类不属于对象,因为对象序列化保存的是对象的 ”状态“,也就是它的成员变量,因此序列化不会关注静态变量。
序列化与反序列化原理
最好理解原理的方式就是带着问题理解。先看以下的 demo 会产生怎样的结果?
public class Person {
private int sex;
private long id;
public Person(int sex, long id) {
this.sex = sex;
this.id = id;
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
@Override
public String toString() {
return "Person{" +
"sex=" + sex +
", id=" + id +
'}';
}
}
public class Account extends Person implements Serializable {
private static final long serialVersionUID = 1;
private String name;
private int age;
public Account(int sex, long id, String name, int age) {
super(sex, id);
this.name = name;
this.age = age;
}
@Override
public String toString() {
return super.toString() + "\nAccount{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
private static void serializeTest() {
ObjectInputStream bis = null;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
Account account = new Account(1, 10, "demo", 27);
oos.writeObject(account);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
bis = new ObjectInputStream(bais);
Account read = (Account) bis.readObject();
System.out.println(read);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
close(bis);
}
}
Build Analyzer results available
java.io.InvalidClassException: com.example.lib.Account; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169)
at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2043)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.example.lib.Test.serializeTest(Test.java:25)
at com.example.lib.Test.main(Test.java:13)
上面定义父类 Person 和子类 Account,其中 Person 没有实现 Serializable,Account 实现了 Serializable,并且 Person 和 Account 各自声明有参构造函数。序列化是编译运行通过。
但在反序列化时会抛出 InvalidClassException:no valid constructor,提示没有有效的构造函数,实际上反序列化需要的是无参构造。我们先把无参构造加上再看下输出结果:
Person{sex=0, id=0}
Account{name='demo', age=27}
可以发现虽然反序列化正常了,但是父类 Person 的数据并没有被正常反序列化出来,子类只管自己的数据序列化。为了解决这个问题,需要在子类 Account 添加两个方法:readObject() 和 writeObject():
public class Account extends Person implements Serializable {
private static final long serialVersionUID = 1;
private String name;
private int age;
public Account() {
}
public Account(int sex, long id, String name, int age) {
super(sex, id);
this.name = name;
this.age = age;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(getSex());
out.writeLong(getId());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
setSex(in.readInt());
setId(in.readLong());
}
@Override
public String toString() {
return super.toString() + "\nAccount{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Person{sex=1, id=10}
Account{name='demo', age=27}
加上 readObject() 和 writeObject() 手动对父类 Person 的成员变量做序列化后,现在反序列化正常了。
这个 demo 出现了两个我们可以思考的问题:
带着上面两个问题,现在开始原理的分析。
序列化过程
Java 使用 ObjectOutputStream 作为序列化工具,通过调用 writeObject() 完成序列化过程,源码分析从它开始:
ObjectOutputStream.java
public final void writeObject(Object obj) throws IOException {
...
try {
writeObject0(obj, false);
} catch (IOException ex) {
...
}
}
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
...
Object orig = obj;
Class<?> cl = obj.getClass();
ObjectStreamClass desc;
for (;;) {
Class<?> repCl;
desc = ObjectStreamClass.lookup(cl, true);
...
}
...
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof 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());
}
}
...
}
ObjectStreamClass.java
static ObjectStreamClass lookup(Class<?> cl, boolean all) {
...
if (entry == null) {
try {
entry = new ObjectStreamClass(cl);
} catch (Throwable th) {
entry = th;
}
...
}
if (entry instanceof ObjectStreamClass) {
return (ObjectStreamClass) entry;
}
...
}
private ObjectStreamClass(final Class<?> cl) {
this.cl = cl;
name = cl.getName();
isProxy = Proxy.isProxyClass(cl);
isEnum = Enum.class.isAssignableFrom(cl);
serializable = Serializable.class.isAssignableFrom(cl);
externalizable = Externalizable.class.isAssignableFrom(cl);
Class<?> superCl = cl.getSuperclass();
superDesc = (superCl != null) ? lookup(superCl, false) : null;
localDesc = this;
if (serializable) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
...
suid = getDeclaredSUID(cl);
try {
fields = getSerialFields(cl);
computeFieldOffsets();
} catch (InvalidClassException e) {
...
}
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);
}
...
return null;
}
});
}
...
initialized = true;
}
private static Long getDeclaredSUID(Class<?> cl) {
try {
Field f = cl.getDeclaredField("serialVersionUID");
int mask = Modifier.STATIC | Modifier.FINAL;
if ((f.getModifiers() & mask) == mask) {
f.setAccessible(true);
return Long.valueOf(f.getLong(null));
}
} catch (Exception ex) {
}
return null;
}
根据上面的源码,当调用 writeObject() 时,首先是通过 ObjectStreamClass.lookup(cl, true) 创建了一个 ObjectStreamClass,通过它的构造函数可以发现,它在构造中将我们传入的对象类进行解析,包括父类的信息,包括类名、serialVersionUID、成员变量字段信息等,并且也检查了 writeObject()、readObject()、readObjectNoData() 这几个方法是否在这个类中声明。
当通过 ObjectStreamClass 将类信息解析完后,在后续步骤调用了 ObjectOutputStream 的 writeOrdinaryObject():
ObjectOutputStream.java
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
...
try {
desc.checkSerialize();
bout.writeByte(TC_OBJECT);
writeClassDesc(desc, false);
handles.assign(unshared ? null : obj);
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
writeSerialData(obj, desc);
}
}
...
}
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
if (slotDesc.hasWriteObjectMethod()) {
PutFieldImpl oldPut = curPut;
curPut = null;
SerialCallbackContext oldContext = curContext;
...
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
}
...
} else {
defaultWriteFields(obj, slotDesc);
}
}
}
ObjectStreamClass.java
void invokeWriteObject(Object obj, ObjectOutputStream out)
throws IOException, UnsupportedOperationException
{
...
if (writeObjectMethod != null) {
try {
writeObjectMethod.invoke(obj, new Object[]{ out });
}
...
}
...
}
ObjectOutputStream 最终处理写的方法是 writeOrdinaryObject(),在序列化时会通过在 ObjectStreamClass 拿到的类信息判断该类是否实现了 writeObject(),如果有则通过反射调用该方法,否则就通过 defaultWriteFields(obj, slotDesc) 写成员变量,类的信息最终都写到 byte[] 数组。
根据上面的序列化分析,在序列化中最重要的就是 ObjectStreamClass,它会解析出待序列化的类的信息,它是用来拆解对象的 Class 信息,包括类名、成员变量信息、serialVersionUID 等,writeObject()、readObject()、readObjectNoData() 是否有实现也会在此获取到。
序列化具体步骤如下:
反序列化过程
Java 使用 ObjectInputStream 作为序列化工具,通过调用 readObject() 完成反序列化过程,源码分析从它开始:
ObjectIntputStream.java
public final Object readObject()
throws IOException, ClassNotFoundException
{
...
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
...
return obj;
}
...
}
private Object readObject0(boolean unshared) throws IOException {
...
try {
switch (tc) {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
...
}
}
...
}
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
...
ObjectStreamClass desc = readClassDesc(false);
...
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
...
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
...
return obj;
}
反序列化过程和序列化是差不多的,ObjectInputStream 最终处理读的方法是 readOrdinaryObject(),在该方法中分别调用了 readClassDesc(false) 、desc.newInstance() 、readSerialData() 三个方法实现将字节数据转换为对象。解析来我们一一查看这三个方法怎么实现的。
首先是 readClassDesc(false):
private ObjectStreamClass readClassDesc(boolean unshared)
throws IOException
{
byte tc = bin.peekByte();
ObjectStreamClass descriptor;
switch (tc) {
...
case TC_CLASSDESC:
descriptor = readNonProxyDesc(unshared);
break;
...
}
...
return descriptor;
}
private ObjectStreamClass readNonProxyDesc(boolean unshared)
throws IOException
{
...
ObjectStreamClass desc = new ObjectStreamClass();
int descHandle = handles.assign(unshared ? unsharedMarker : desc);
passHandle = NULL_HANDLE;
ObjectStreamClass readDesc = null;
try {
readDesc = readClassDescriptor();
} catch (ClassNotFoundException ex) {
throw (IOException) new InvalidClassException(
"failed to read class descriptor").initCause(ex);
}
...
try {
...
desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
}
...
return desc;
}
protected ObjectStreamClass readClassDescriptor()
throws IOException, ClassNotFoundException
{
ObjectStreamClass desc = new ObjectStreamClass();
desc.readNonProxy(this);
return desc;
}
ObjectStreamClass.java
void readNonProxy(ObjectInputStream in)
throws IOException, ClassNotFoundException
{
name = in.readUTF();
suid = Long.valueOf(in.readLong());
isProxy = false;
byte flags = in.readByte();
hasWriteObjectData =
((flags & ObjectStreamConstants.SC_WRITE_METHOD) != 0);
hasBlockExternalData =
((flags & ObjectStreamConstants.SC_BLOCK_DATA) != 0);
externalizable =
((flags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0);
boolean sflag =
((flags & ObjectStreamConstants.SC_SERIALIZABLE) != 0);
if (externalizable && sflag) {
throw new InvalidClassException(
name, "serializable and externalizable flags conflict");
}
serializable = externalizable || sflag;
isEnum = ((flags & ObjectStreamConstants.SC_ENUM) != 0);
...
int numFields = in.readShort();
...
fields = (numFields > 0) ?
new ObjectStreamField[numFields] : NO_FIELDS;
for (int i = 0; i < numFields; i++) {
char tcode = (char) in.readByte();
String fname = in.readUTF();
String signature = ((tcode == 'L') || (tcode == '[')) ?
in.readTypeString() : new String(new char[] { tcode });
try {
fields[i] = new ObjectStreamField(fname, signature, false);
} catch (RuntimeException e) {
throw (IOException) new InvalidClassException(name,
"invalid descriptor for field " + fname).initCause(e);
}
}
computeFieldOffsets();
}
void initNonProxy(ObjectStreamClass model,
Class<?> cl,
ClassNotFoundException resolveEx,
ObjectStreamClass superDesc)
throws InvalidClassException
{
long suid = Long.valueOf(model.getSerialVersionUID());
ObjectStreamClass osc = null;
if (cl != null) {
osc = lookup(cl, true);
...
}
this.cl = cl;
this.resolveEx = resolveEx;
this.superDesc = superDesc;
name = model.name;
this.suid = suid;
isProxy = false;
isEnum = model.isEnum;
serializable = model.serializable;
externalizable = model.externalizable;
hasBlockExternalData = model.hasBlockExternalData;
hasWriteObjectData = model.hasWriteObjectData;
fields = model.fields;
primDataSize = model.primDataSize;
numObjFields = model.numObjFields;
if (osc != null) {
localDesc = osc;
writeObjectMethod = localDesc.writeObjectMethod;
readObjectMethod = localDesc.readObjectMethod;
readObjectNoDataMethod = localDesc.readObjectNoDataMethod;
writeReplaceMethod = localDesc.writeReplaceMethod;
readResolveMethod = localDesc.readResolveMethod;
if (deserializeEx == null) {
deserializeEx = localDesc.deserializeEx;
}
domains = localDesc.domains;
cons = localDesc.cons;
}
fieldRefl = getReflector(fields, localDesc);
fields = fieldRefl.getFields();
initialized = true;
}
上面的步骤其实就只有一个处理:将序列化的字节信息读取出来,然后手动构建一个 ObjectStreamClass 存放类信息。
接下来是 desc.newInstance():
ObjectStreamClass.java
Object newInstance()
throws InstantiationException, InvocationTargetException,
UnsupportedOperationException
{
...
if (cons != null) {
try {
if (domains == null || domains.length == 0) {
return cons.newInstance();
} else {
JavaSecurityAccess jsa = SharedSecrets.getJavaSecurityAccess();
PrivilegedAction<?> pea = () -> {
try {
return cons.newInstance();
} catch (InstantiationException
| InvocationTargetException
| IllegalAccessException x) {
throw new UndeclaredThrowableException(x);
}
};
...
}
}
....
}
...
}
desc.newInstance() 实际上就是反射调用的无参构造函数,所以从这里也可以看到,反序列化创建的对象为什么需要一个无参数构造。
最后是 readSerialData():
private void readSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
if (slots[i].hasData) {
if (obj == null || handles.lookupException(passHandle) != null) {
defaultReadFields(null, slotDesc);
} else if (slotDesc.hasReadObjectMethod()) {
...
try {
...
slotDesc.invokeReadObject(obj, this);
}
...
} else {
defaultReadFields(obj, slotDesc);
}
...
}
...
}
}
readSerialData() 的处理是,通过在 ObjectStreamClass 拿到的类信息判断该类是否实现了 readObject(),如果有则通过反射调用该方法,否则就通过 defaultReadFields(obj, slotDesc) 读取成员变量。最后将反序列化转换的对象返回到上层。
反序列化具体步骤如下:
-
在反序列化时手动构建一个 ObjectStreamClass 承载类信息(如果对象序列化时是写入文件,则通过文件路径读取字节数据转换为类信息) -
desc.newInstance() 调用类的无参构造函数,接着开始读数据反序列化 -
在将字节数据读取为类信息时,如果类有实现了 readObject(),则会通过反射调用 -
将字节数据转换为类成员变量字段
小结
根据序列化和反序列化的过程原理解析,可以回答提出的两个问题:
1、为什么反序列化时要有无参构造函数?
因为在反序列化时是直接通过反射调用的无参构造函数创建对象,其他有参构造不会被调用。
2、父类没有实现 Serializable,它的成员变量如果要被序列化,需要加上 readObject() 和 writeObject(),为什么加上它们就能正常了?这两个方法是什么时候被调用的?
writeObject() 和 readObject() 分别对应的序列化和反序列化过程的其中一个环节,在构建 ObjectStreamClass 解析类信息时会检查是否有实现这两个方法,在序列化写入和反序列化读取字节数据时,即 ObjectOutputStream 的 writeSerialData() 和 ObjectInputStream 的 readSerialData(),如果检查到类有实现这两个方法,会通过反射调用 writeObject() 和 readObject(),这两个方法可以理解为是干预了序列化和反序列化过程。
所以,通过 writeObject() 和 readObject() 这两个方法,也可以实现如果子类不想继承父类的序列化,打断序列化和反序列化,可以这么写:
public class Account extends Person {
private void writeObject(ObjectOutputStream out) throws IOException {
throw new NotSerializableException("Can not serialize this class");
}
private void readObject(ObjectIntputStream is) throws IOException, ClassNotFoundException {
throw new NotSerializableException("Can not serialize this class");
}
private void readObjectNoData() throws ObjectStreamException {
throw new NotSerializableException("Can not serialize this class");
}
}
序列化和反序列化的本质原理就是将对象拆成字节数据和将字节数据恢复成对象的过程。
Serializable 与 Parcelable
Serializable 是 Oracle 提供的一套序列化方案,在 Java 的角度更多的是考虑网络数据的传递,Serializable 是基于 IO 流,在序列化和反序列化过程中会存在大量反射操作创建较多的临时对象,内存碎片比较多容易触发垃圾回收,IO 也会造成额外的 CPU 开销。
在 Android 场景对性能要求较高,且 Android 系统内部应用了跨进程方案对各个组件进行管理,会存在大量的进程间通信,Serializable 的序列化方案在 Android 使用会存在三个问题:大量使用反射序列化和反序列化产生大量对象、跨进程通信场景性能较差、基于 IO 造成额外开销。
为了解决上述问题问题,Google 自己提供了一套序列化方案:Parcelable。在数据传输时使用 Parcel 包装数据,可以将序列化之后的数据写入到一个共享内存中,其他进程通过 Parcel 可以从这块共享内存中读出字节流并反序列化为对象。Parcel 的数据在 Binder 中传输。
Parcelable 的核心目的是为了解决跨进程通信场景的数据传输问题,而不是为了网络传输与持久化保存数据,为了解决 Serializable 反射产生大量对象的问题,Parcelable 将对象创建放在 native 层减少了内存碎片;而解决 Serializable 基于 IO 引发的性能损耗,Parcelable 依托于 Binder 机制,去掉 IO,将数据的传输放在内存处理。
类型 | Serializable | Parcelable |
---|
操作方案 | 基于 IO 速度慢 | 直接在内存操作,效率高 | 数据大小 | 大小不受限制 | 基于 Binder 机制,数据大小不超过 1M(更具体说是 1M-8k) | 其他 | 大量反射,内存碎片多 | |
序列化相关常见问题
1、什么是 serialVersionUID?如果不定义这个,会发生什么?
serialVersionUID 是 Serializable 序列化方案中用于做版本兼容的,当一个类实现了 Serializable 接口但没有手动提供 serialVersionUID 时,在序列化时会通过类信息信息计算出一个 serialVersionUID,但这会导致如果序列化前后字段变更,就会导致反序列化失败抛出 InvalidClassException;手动指定 serialVersionUID 确保类的字段变更时也能正常反序列化,但变更的字段会是初始值。
2、序列化时,如果希望某些成员不参与序列化,你该如何实现?
不参与序列化的成员变量可以加上 transient 关键字或者声明为静态变量。声明为 transient 的成员变量因为序列化不参与,所以反序列化时是初始值;静态变量因为是归属于类而不归属于对象所以能实现同样的效果,序列化是对 “对象” 状态的处理,而非对类的状态处理。
3、如果要序列化的类中有一个成员变量未实现 Serializable,会发生什么情况?
会抛出 NotSerializableException 无法正常序列化。
4、如果类是可序列化的,但其父类不是,可以序列化吗?如果可以序列化,反序列化后从父类继承的成员变量字段状态如何?
子类实现了 Serializable 但父类没有是可以正常序列化的,但是反序列化时从父类继承的字段因为没有参与序列化,所以成员变量的字段是初始值;如果要父类的字段也参与序列化,需要手动添加 writeObject() 和 readObject() 干预序列化和反序列化过程。
5、为什么反序列化一定需要一个无参构造函数?
因为反序列化时是直接 constructor.newInstance() 反射调用的无参构造函数创建的对象,所以每个需要序列化的对象都需要提供一个无参构造函数,否则会反序列化失败。
6、是否可以自定义序列化过程,或者覆盖 Java 中默认的序列化过程?
可以,在序列化和反序列化过程中会去检查对象是否有实现 writeObject()、readObject() 干预序列化和反序列化过程。
7、假设子类的父类实现了 Serializable,子类如何避免序列化?
因为序列化是有继承关系的,所以子类继承父类时同样会把序列化也继承过来,如果有需要子类不参与序列化的需求,可以在子类实现 readObject()、writeObject(),在方法内抛出异常避免序列化:
public class Account extends Person {
private void writeObject(ObjectOutputStream out) throws IOException {
throw new NotSerializableException("Can not serialize this class");
}
private void readObject(ObjectIntputStream is) throws IOException, ClassNotFoundException {
throw new NotSerializableException("Can not serialize this class");
}
private void readObjectNoData() throws ObjectStreamException {
throw new NotSerializableException("Can not serialize this class");
}
}
8、序列化和持久化的关系和区别是什么?
序列化的本质是为了做数据传输,持久化的本质是保存数据,只是 Serializalbe 序列化是基于 IO 流可以做到将对象做保存。
9、序列化的对象与序列化后的对象是什么关系?
因为反序列化时是通过反射调用无参构造函数创建的对象,所以序列化前后的对象是不一样的,更确切的说反序列化是深拷贝过程:
private static void serializeTest() {
ObjectInputStream bis = null;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
Account account = new Account(1, 10, "demo", 27);
oos.writeObject(account);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
bis = new ObjectInputStream(bais);
Account account2 = (Account) bis.readObject();
System.out.println("is reference equal = " + (account == account2));
System.out.println("is equal = " + (account.equals(account2)));
System.out.println("hashcode, account = " + account.hashCode() + ", read = " + account2.hashCode());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
close(bis);
}
}
输出:
is reference equal = false
is equal = false
hashcode, account = 1975012498, read = 1747585824
10、Serializable 和 Parcelable 的本质区别?
Serializable 是基于 IO 流做数据传输,序列化和反序列化过程会通过大量的反射创建对象。Parcelable 是基于 Binder 机制直接在共享内存做数据传输,序列化和反序列化过程对象创建放在 native 层减少像 Serializable 使用反射产生大量对象导致较多内存碎片。
|