序列化 - Java
Java序列化框架是一种Java专有的非通用的序列化方案,这是和protobuf、avro、json等通用序列化框架的根本区别。除此之外,Java的序列化更慢、序列化后的体积更大,所以即使是在Java里,应用也没以上通用序列化框架广泛。
Java如何序列化反序列化
序列化样例:
1
2
3
4
5
6
7
8
9
10
11
12
13
String location = "/tmp/people.ser";
try {
FileOutputStream fos = new FileOutputStream(location);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(inputStudent);
oos.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
反序列化样例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
try {
FileInputStream fis = new FileInputStream(location);
ObjectInputStream ois = new ObjectInputStream(fis);
// readObject() -> ClassNotFoundExcception
// For a JVM to be able to deserialize an object,
// it must be able to find the bytecode for the class
Student resStudent = (Student)ois.readObject();
// SerializeDemo.Student(name=Lily, age=18, think=null, dreams=[eat, play])
System.out.println(resStudent);
ois.close();
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
序列化 - write
- DataOutput:定义了写基本类型的接口,比如writeChar/Int/Boolean/Byte等;
- ObjectOutput:定义了写Object的接口,继承DataOutput接口;
- ObjectOutputStream:实现了ObjectOutput接口,拥有writeObject的实现。
那就看这个writeObject怎么实现的:
- writeObject(Object)
- writeObject0
- writeOrdinaryObject
- writeSerialData或者writeExternalData
- 如果是writeSerialData,继续调用invokeWriteObject或者defaultWriteFields
玄机都藏在这几步里:
为什么想要序列化的类必须实现Serializable接口
在writeObject0里有以下几步:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// remaining cases
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());
}
}
所以一个类如果不实现Serializable接口,最终会落到else里,抛出NotSerializableException。
都序列化了什么东西
在writeOrdinaryObject里,有如下代码:
1
2
3
4
5
6
7
8
9
10
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);
}
所以写了:
- 一个专属于Object的magic byte(String、Enum之类的用其他的magic byte);
- 类描述信息;
- 真实数据信息;
其中,类描述信息是ObjectStreamClass类,它里面放了要序列化的对象的类信息,比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** class associated with this descriptor (if any) */
private Class<?> cl;
/** name of class represented by this descriptor */
private String name;
/** serialVersionUID of represented class (null if not computed yet) */
private volatile Long suid;
/** true if represents dynamic proxy class */
private boolean isProxy;
/** true if represents enum type */
private boolean isEnum;
/** true if represented class implements Serializable */
private boolean serializable;
/** true if represented class implements Externalizable */
private boolean externalizable;
/** true if desc has data written by class-defined writeObject method */
private boolean hasWriteObjectData;
...
大致有:
- 类名;
- 类的serial version id(实现了Serializable接口,就得有这个id);
- 其他很多辅助信息;
最后使用defaultWriteFields方法真正序列化对象的时候:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Fetches and writes values of serializable fields of given object to
* stream. The given class descriptor specifies which field values to
* write, and in which order they should be written.
*/
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();
}
}
}
}
按照类描述里的内容,决定写那些field,按什么顺序写field。所以还是靠的反射。
所以:
- Java序列化后的体积为什么比其他序列化(avro、protobuf、json)框架大?因为写了很多额外信息;
- Java序列化的速度为什么比其他序列化框架慢?因为写的东西多,做的检查多,执行步骤多;
- Java序列化为什么不能跨语言?因为不止写了数据信息,还加入了乱七八糟的只有Java才有的信息;
其他序列化框架写了啥?
- json一般就写了属性和对应的数据。当然也可能加入其他metadata,比如fastjson还可能写入了属性的实际类型信息(方便对多态反序列化);
- avro、protobuf一类的写的是字节,而且写的东西更少。比如protobuf只写属性代号和属性值,连属性名都不序列化。属性名在反序列化的时候根据代号去schema里查。所以序列化后的一坨字节很小,而且只有他们这些框架本身能理解;
当然还有其他优化操作:
- avro是先写一个schema,写对象的时候只写各个value的内容,按照schema字段的顺序写的,免去了写key。protobuf是写id:value的键值对,每个id对应一个字段,且不可修改。反序列化的时候,按照代码的id去序列化为相应字段;
- avro和protobuf都需要先编译schema生成schema定义的对象的专用代码,然后用该对象的专用代码去序列化反序列化对象。Java序列化则不单独为某对象生成相应的序列和反序列化代码,而是使用反射,这应该也是Java序列化更慢一些的原因。(不过好处是Java不需要提前单独编译类似protobuf/avro的schema生成相应代码)
Ref:
- protobuf序列化:https://puppylpg.github.io/protobuf/serialization/2020/05/15/serialization-protobuf.html
- fastjson的一些序列化:https://hollis.blog.csdn.net/article/details/107150646;
- Java原生序列化为什么慢:https://my.oschina.net/u/1787735/blog/1919855
Externalizable - @Deprecated
曾经Java序列化巨慢(1.3之后就好多了),所以Java提供了Externalizable接口,由用户自定义readExternal/writeExternal接口,而不是Java用反射序列化反序列化Java类里的field。这样会快一些,但是所有的逻辑都是用户自己维护了,如果增删字段,也要修改这些方法。
这就是序列化的时候,调用writeExternal方法所做的事。
Java序列化优化之后,就没太必要用这个了。不过可以作为一种拓展吧。
- https://stackoverflow.com/a/818093/7676237
自定义序列化方式
在序列化最后真正写数据的时候,invokeWriteObject里还有这样的代码:
1
writeObjectMethod.invoke(obj, new Object[]{ out })
调用了一个反射去写对象。方法是:
1
2
/** class-defined writeObject method, or null if none */
private Method writeObjectMethod;
该方法来自于:
1
2
3
4
5
6
writeObjectMethod = getPrivateMethod(
cl,
"writeObject",
new Class<?>[] { ObjectOutputStream.class },
Void.TYPE
);
被写对象的writeObject!
所以Java序列化框架给了序列化对象自己序列化自己的机会!
有什么用呢?比如ArrayList底层用的数组,快满时会扩容。序列化的时候最好只写已存放的数据。如果把整个数组都序列化了,岂不是存了一大堆null……所以自己如何序列化自己最清楚。
如果自己没定义writeObject方法呢? 那writeSerialData就会调用defaultWriteFields方法,进行Java序列化框架默认的序列化。
Ref:
- Java序列化:https://www.hollischuang.com/archives/1140
反序列化 - read
- DataInput
- ObjectInput
- ObjectInputStream
总结
- Java序列化不能跨语言;
- Java序列化体积大速度慢是有原因的;
- Java序列化为Java的自主序列化和反序列化做了很多事情,远不是其他序列化平台那样直接写数据那么简单。