序列化和反序列化是什么 what???
Java序列化是把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程。
在网络传输过程中,需要以字节流的形式来传输数据;而字节的可以还原成完全相等的对象,这个相反的过程又称为反序列化;
- 序列化:对象序列化的主要用处就是在传递和保存对象的完整性和可传递性。序列化是把对象转换为有序字节流,以便在网络上传输或保存在本地文件中。核心作用是对象的保存和重建。
- 反序列化:客户端从文件中或网络上获得序列化后的对象字节流,根据字节流的状态和描述信息,通过反序列化重建对象。
序列化和反序列化的好处以及为什么要序列化 Why?
一、对象序列化可以实现分布式对象
例如:远程方法调用(RMI)和远程过程调用(RPC)要利用对象序列化运行远程主机上的服务,就像在本机上运行一样,先序列化为有序字节流传输到服务器主机,服务器主机再拿到有序字节流进行反序列化拿到对象数据。
二、Java对象序列化不仅可以保存一个对象的数据,而且可以递归保存对象引用的每个对象的数据。
可以将整个对象层次的写入字节流中,可以保存在文件中或在网络中进行传递。利用对象序列化可以进行对象的“深拷贝”,即拷贝对象的引用和堆中的数据,序列化一个对象可以得到整个对象序列。
三、序列化可以将内存中的类写入文件或数据库中(持久化功能)
例如:将某个类序列化后存为文件,下次读取时只需将文件中的数据反序列化就可以将原来的类还原到内存中,也可以将类序列化为流数据进行传输。
四、对象、文件、数据,有许多不同的格式,很难统一传输和保存
序列化之后都是字节流了,无论原来是什么东西,都变成一样的东西,就可以进行通用的格式传输和保存,传输结束之后,就反序列化进行还原,这样对象好使对象文件还是文件。
怎样使用序列化 How?
序列化算法一般会按步骤:
•将对象实例相关的类元数据输出。
•递归地输出类的超类描述直到不再有超类。
•类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
•从上至下递归输出实例的数据
序列化种类:
Java原生序列化
序列化的类需要先继承Serializable接口
public class Student implements Serializable {
private static final long serialVersionUID = -2991763699601905391L;
private String name;
private String score;
public Student(String name, String score) {
this.name = name;
this.score = score;
}
public void setName(String name) {
this.name = name;
}
public void setScore(String score) {
this.score = score;
}
public String getName() {
return name;
}
public String getScore() {
return score;
}
}
序列化到文件中,和从序列化文件中取出数据
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//默认序列化
FileOutputStream out = new FileOutputStream("1.out");
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(new Student("张三","30"));
oos.flush();
oos.close();
//默认反序列化
FileInputStream fis = new FileInputStream("1.out");
ObjectInputStream ois = new ObjectInputStream(fis);
Student o = (Student) ois.readObject();
System.out.println("名字是:"+o.getName()+"\t年龄是:"+o.getScore());
ois.close();
}
}
运行结果序列化生成了一个1.out文件,反序列化后输出结果如下:
序列化版本号 ‘serialVersionUID’ 的作用:
1、JVM在序列化过程识别不同类依据。当序列化版本号不同JVM认为这是两个不同的类)
即使类的名字相同
2、JVM在识别不同类时先比较类名字(name),如果相同则比较序列号版本号;
类的名字就好像人身份证上的名字。重名的人有很多个,但要辨别是不是同一个人,查看身份证号即可
3、class类中实现Serializable接口时JVM会随机给一个序列号版本号
也可主动进行赋值private static final long serialVersionUID=1L;(一般主动给与序列化版本号给一个1L即可)
在已经序列化的文件, 但他类已经修改时,如果序列化版本号不一致会异常InvalidClassException
修改类的一些情况
序列号版本相同的情况下
新修改的类增加了原来类没有的变量,在原来类已经序列化,在反序列化新增加的变量为默认值
新修改的类删除了原来类的变量,在反序列化以前的类时,该变量也会跟着消失
transient关键字
transient在类中修饰的变量的数据,不参与序列化的过程,反序列化后该变量为java默认值,比如 String s=“11”; 字符串,在序列化和反序列化后 ,得到的是String s=null;
为什么会使用transient关键字:
1、可以避免序列化浪费不必要的资源
2、降低安全问题,防止反序列化时中间人向里面加一些数据
原理:
调用ObjectOutputStream.writeObject()和ObjectInputStream.readObject()之后究竟做了什么?
1. ObjectStreamClass类
它是类的序列化描述符,这个类可以描述需要被序列化的类的元数据,包括被序列化的类的名字以及序列号。可以通过lookup()方法来查找/创建在这个JVM中加载的特定的ObjectStreamClass对象。
2.序列化:writeObject()
调用writeObject()进行序列化之前会先调用ObjectOutputStream的构造函数生成一个ObjectOutputStream对象,构造函数如下:
public ObjectOutputStream(OutputStream out) throws IOException {
verifySubclass();
// bout表示底层的字节数据容器
bout = new BlockDataOutputStream(out);
handles = new HandleTable(10, (float) 3.00);
subs = new ReplaceTable(10, (float) 3.00);
enableOverride = false;
writeStreamHeader(); // 写入文件头
bout.setBlockDataMode(true); //flush数据
if (extendedDebugInfo) {
debugInfoStack = new DebugTraceInfoStack();
} else {
debugInfoStack = null;
}
}
构造函数中首先会把bout绑定到底层的字节数据容器,接着会调用writeStreamHeader()方法,在writeStreamHeader()方法中首先会往底层字节容器中写入表示序列化的Magic Number以及版本号,该方法实现如下:
protected void writeStreamHeader() throws IOException {
bout.writeShort(STREAM_MAGIC);
bout.writeShort(STREAM_VERSION);
}
接下来会调用writeObject()方法进行序列化,实现如下:
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
// 调用writeObject0()方法序列化
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
正常情况下会调用writeObject0()进行序列化操作,该方法实现如下:
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try {
//处理以前写入和不可替换的对象
// handle previously written and non-replaceable objects
int h;
if ((obj = subs.lookup(obj)) == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
//检查替换对象
// check for replacement object
Object orig = obj;
//获取要序列化的对象的Class对象
Class<?> cl = obj.getClass();
ObjectStreamClass desc;
for (;;) {
// REMIND: skip this check for strings/arrays?
Class<?> repCl;
// 创建描述cl的ObjectStreamClass对象
desc = ObjectStreamClass.lookup(cl, true);
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) == null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl;
}
if (enableReplace) {
Object rep = replaceObject(obj);
if (rep != obj && rep != null) {
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}
obj = rep;
}
// 如果对象被替换,则再次执行原始检查
// if object replaced, run through original checks a second time
if (obj != orig) {
subs.assign(orig, obj);
if (obj == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
}
// 根据实际的类型进行不同的写入操作
// 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) {
// 被序列化对象实现了Serializable接口
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
depth--;
bout.setBlockDataMode(oldMode);
}
}
在里面会先调用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();
// 写入Object标志位
bout.writeByte(TC_OBJECT);
// 写入类元数据
writeClassDesc(desc, false);
handles.assign(unshared ? null : obj);
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj); // 写入被序列化的对象的实例数据
} else {
// 写入被序列化的对象的实例数据
writeSerialData(obj, desc);
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
方法里面会先调用writeClassDesc()方法写入被序列化对象的类的类元数据
private void writeClassDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
int handle;
if (desc == null) {
// 如果desc为null
writeNull();
} else if (!unshared && (handle = handles.lookup(desc)) != -1) {
writeHandle(handle);
} else if (desc.isProxy()) {
writeProxyDesc(desc, unshared);
} else {
writeNonProxyDesc(desc, unshared);
}
}
之后一般情况下接下来会调用writeNonProxyDesc()方法,该方法实现如下:
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
// TC_CLASSDESC = (byte)0x72;
// 表示一个新的Class描述符
bout.writeByte(TC_CLASSDESC);
handles.assign(unshared ? null : desc);
if (protocol == PROTOCOL_VERSION_1) {
// 写入类元信息
// do not invoke class descriptor write hook with old protocol
desc.writeNonProxy(this);
} else {
writeClassDescriptor(desc);
}
Class cl = desc.forClass();
bout.setBlockDataMode(true);
if (cl != null && isCustomSubclass()) {
ReflectUtil.checkPackageAccess(cl);
}
annotateClass(cl);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
//递归调用 如果有父类就递归调用,写入父类元数据
writeClassDesc(desc.getSuperDesc(), false);
}
在这个方法中首先会写入一个字节的TC_CLASSDESC,这个字节表示接下来的数据是一个新的Class描述符,接着会调用writeNonProxy()方法写入实际的类元信息,执行完上面的过程之后,程序流程重新回到writeNonProxyDesc()方法中,之后就会调用 writeClassDesc递归调用写入父类元数据,直到writeClassDesc()里面进入desc==null时,表示没有父类退出递归。
在递归调用完成写入类的类元数据之后,程序执行流程回到wriyeOrdinaryObject()方法中,
接下来会调用writeSerialData()方法写入被序列化的对象的字段的数据
在这个方法中会通过defaultWriteFields()方法进行数据的写入:
private void defaultWriteFields(Object obj, ObjectStreamClass desc)
throws IOException
{
// 其他一些省略代码
int primDataSize = desc.getPrimDataSize();
if (primVals == null || primVals.length < primDataSize) {
primVals = new byte[primDataSize];
}
// 获取对应类中的基本数据类型的数据并保存在primVals字节数组中
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;
// 把对应类的Object类型(非原始类型)的对象保存到objVals数组中
desc.getObjFieldValues(obj, objVals);
for (int i = 0; i < objVals.length; i++) {
// 一些省略的代码
try {
// 对所有Object类型的字段递归调用writeObject0()方法写入对应的数据
writeObject0(objVals[i],
fields[numPrimFields + i].isUnshared());
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
}
1、在这个方法里面首先会对父类里的数据进行写入,而这个写入过程也是利用了反射机制
2、在这个过程中会做下面几件事情:
<1> 获取对应类的基本类型的字段的数据,并写入到底层的字节容器中。
<2> 获取对应类的Object类型(非基本类型)的字段成员,递归调用writeObject0()方法写入相应的数据。
3、再进行子类数据的写入
从上面对写入数据的分析可以知道,写入数据是是按照先父类后子类的顺序来写的。
序列化过程总结如下图:
通过源码可以知道,原生序列化方式信息是比较全面的,但是数据量也是非常大了,如果要进行网络传输,那肯定在性能方面不行的,首先在序列化时就会很慢,而且数据量大在网络中传输也是笔比较耗时间的 ,并且不能跨语言。
基于性能及兼容性考虑,不推荐使用 Java 原生序列化。
json序列化
JSON是典型的key-value形式,没有数据类型,是一种文本型序列化框架
在序列化过程中抛弃了类型信息,所以反序列化时只有提供类型信息才能准确地反序列化。相比前两种方式,JSON 可读性比较好,方便调试。
基于HTTP的RPC通信框架会采用JSON格式,存在以下问题:
JSON序列化的额外空间开销比较大,对于大数据量服务这意味着巨大的内存和磁盘开销,所以选择JSON的时候,数据量要小。
JSON没有类型,像Java这种强类型语言,转换为类对象时,需要反射统一解决,性能不太好
fastjson序列化
FastJson数据处理速度快,无论序列化(把javabean对象转化成Json格式的字符串)和反序列化(把JSON格式的字符串转化为Java Bean对象),都是当之无愧的快
功能强大(支持普通JDK类,包括javaBean, Collection, Date 或者enum)
零依赖(没有依赖其他的任何类库)
1、利用了threadlocal存储buf来优化内存
2、使用了asm观察者模式,避免使用反射消耗的时间
3、使用了identityhashmap结构,存储的key,再序列化key时直接取就可以,避免了重复序列化
4、sorted field:让类里面的属性字节码存储是有顺序的,反序列化时也是顺序反序列化。避免了比较谁先谁后而造成的耗时操作。
5、token预判:比如大括号 '{' 后面加引号,中括号 ‘[’ 后面加大括号,避免一些重复判断和无效的耗时判断
6、symbol table:作为缓存使用减少反序列化操作,把常用的string数据缓存在里面
7、自行编写类似StringBuilder的工具类SerializeWriter。
1. 可读性好,空间占用小
2. 特点:弱类型,序列化结果不携带类型信息,可读性强。有一些安全性问题
3. 反序列化:基于 Field 机制,兼容 Bean 机制
4. 应用场景:消息、透传对象
hessian序列化
Hession是动态类型、二进制、紧凑的、可跨语言移植的一种序列化框架。
Hessian协议要比JDK、JSON更加紧凑,性能要比JDK、JSON序列化高效很多,而且生成的字节数也更小。
Java 对象序列化的二进制流可以被其他语言 (C++/Python)反序列化。 Hessian 协议具有如下特性:自描述序列化类型。不依赖外部描述文件或接口定义用一个字节表示常用基础类型,
- 极大缩短二进制流。
- 语言无关,支持脚本语言。
- 协议简单,比 Java 原生序列化高效。
相比 Hessian 1.0, Hessian 2.0 中增加了压缩编码,其序列化二进制流大小是 Java序列化的 50%, 序列化耗时是 Java 序列化的 30 %,反序列化耗时是 Java 反序列化的20%
Hessian 会把复杂对象所有属性存储在一个 Map 进行序列化.
所以在父类、子类存在同名成员变量的情况下, Hessian 序列化时,先序列化子 ,然后序列化父类,因此反序列化结果会导致子类同名成员变量被父类的值覆盖。
效率高,兼容性高,推荐rpc使用。
Protobuf
Google内部的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。
Protobuf 使用的时候需要定义 IDLProtobuf使用的时候需要定义IDL文件,使用不用语言的IDL编译器,生成序列化工具类。
序列化体积比JSON、Hessian要小
IDL能清晰的描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器;
序列化、反序列化速度很快,不需要反射获取类型
消息格式升级和兼容性不错,可以做到向后兼容。
// IDl 文件格式
synax = "proto3";
option java_package = "com.test";
option java_outer_classname = "StudentProtobuf";
message StudentMsg {
int32no=1;
//姓名
string name = 2;
}
Protobuf 非常高效,但是对于具有反射和动态能力的语言来说,这样用起来很费劲,这一点就不如 Hessian,比如用 Java 的话,这个预编译过程不是必须的,可以考虑使用 Protostuff。
Protostuff
Protostuff 不需要依赖 IDL 文件,可以直接对 Java 领域对象进行反 / 序列化操作,在效率上跟 Protobuf 差不多,生成的二进制格式和 Protobuf 是完全相同的,可以说是一个 Java 版本的 Protobuf 序列化框架。但是不支持null,不支持Map、List集合对象,需要包在对象里。
序列化和反序列化比较
目录