序列化
最后更新于
序列化是指将一个对象转换为一个字节序列(包含对象的数据、对象的类型和对象中存储的属性等信息),以便在网络上传输或保存到文件中,或者在程序之间传递。在 Java 中,序列化通过实现 java.io.Serializable
接口来实现,只有实现了 Serializable 接口的对象才能被序列化。
反序列化是指将一个字节序列转换为一个对象,以便在程序中使用。
Java 的序列流(ObjectInputStream 和 ObjectOutputStream)是一种可以将 Java 对象序列化和反序列化的流。
一个对象要想序列化,必须满足两个条件:
该类必须实现java.io.Serializable
,否则会抛出NotSerializableException
。
该类的所有字段都必须是可序列化的。如果一个字段不需要序列化,则需要使用transient
关键字open in new window进行修饰。
使用示例如下:
java.io.ObjectOutputStream
继承自 OutputStream 类,因此可以将序列化后的字节序列写入到文件、网络等输出流中。
来看 ObjectOutputStream 的构造方法,接收一个 OutputStream 对象作为参数,用于将序列化后的字节序列输出到指定的输出流中。例如:
具体用法如下:
ObjectInputStream 可以读取 ObjectOutputStream 写入的字节流,并将其反序列化为相应的对象(包含对象的数据
、对象的类型
和对象中存储的属性
等信息)。
说简单点就是,序列化之前是什么样子,反序列化后就是什么样子:
实际开发中,很少使用 JDK 自带的序列化和反序列化,这是因为:
可移植性差:Java 特有的,无法跨语言进行序列化和反序列化。
性能差:序列化后的字节体积大,增加了传输/保存成本。
安全问题:攻击者可以通过构造恶意数据来实现远程代码执行,从而对系统造成严重的安全威胁。
Kryo 是一个优秀的 Java 序列化和反序列化库,具有高性能、高效率和易于使用和扩展等特点,有效地解决了 JDK 自带的序列化机制的痛点。
序列化有一条规则,就是要序列化的对象必须实现 Serializbale
接口,否则就会报 NotSerializableException 异常。
但是,Serializbale
接口的定义是空的,如何能够保证实现了它的“类对象”被序列化和反序列化?
如果没有实现 Serializbale
接口,在运行测试类的时候会抛出异常,堆栈信息如下:
顺着堆栈信息,我们来看一下 ObjectOutputStream
的 writeObject0()
方法。其部分源码如下:
Serializable
接口之所以定义为空,是因为它只起到了一个标识的作用,告诉程序实现了它的对象是可以被序列化的,但真正序列化和反序列化的操作并不需要它来完成。
反序列化:以 ObjectInputStream
为例,它在反序列化的时候会依次调用 readObject()
→readObject0()
→readOrdinaryObject()
→readSerialData()
→defaultReadFields()
。
不是所有的字段都需要序列化,例如用户的一些敏感信息(如密码、银行卡号等),为了安全起见,不希望在网络操作中传输或者持久化到磁盘文件中,那这些字段就可以加上 transient
关键字。
static 和 transient 修饰的字段是不会被序列化的。我们写个例子:
测试代码:
输出:
序列化前,pre
的值为“沉默”,序列化后,pre
的值修改为“不沉默”,反序列化后,pre
的值为“不沉默”,而不是序列化前的状态“沉默”。因为序列化保存的是对象的状态,而 static
修饰的字段属于类的状态,因此可以证明序列化并不保存 static
修饰的字段。
序列化前,meizi
的值为“王三”,反序列化后,meizi
的值为 null
,而不是序列化前的状态“王三”。因为transient
(即临时的),它可以阻止字段被序列化到文件中,在被反序列化后,transient
字段的值被设为初始值,比如 int
型的初始值为 0,对象型的初始值为 null
。
在源码中,被Modifier.STATIC | Modifier.TRANSIENT
这两个修饰符标记的字段就没有被放入到序列化的字段中:
除了 Serializable
之外,Java 还提供了一个序列化接口 Externalizable
Serializable 是 Java 标准库提供的接口,而 Externalizable 是 Serializable 的子接口
与Serializable相比,新增了一个无参的构造方法。因为使用 Externalizable
进行反序列化的时候,会调用被序列化类的无参构造方法去创建一个新的对象,然后再将被保存对象的字段值复制过去。
新增了两个方法 writeExternal()
和 readExternal()
,实现 Externalizable
接口所必须的。可以自由控制读写哪些字段,也可以对对象进行自定义的处理,如对一些敏感信息进行加密和解密。
我们实现一下:
测试类:
怎么都变成了默认值?因为我们没有为 Wanger
类重写具体的 writeExternal()
和 readExternal()
方法。那该怎么重写呢?
在不被序列化的static 和 transient一节中,有一行神秘的代码:
当一个类实现了 Serializable
接口后,IDE 就会提醒该类最好产生一个序列化 ID,即serialVersionUID
,它是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,Java 虚拟机会把字节流中的 serialVersionUID
与被序列化类中的 serialVersionUID
进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。
生成序列化 ID的方法有三种:
如果没有特殊需求,采用默认的序列化 ID(1L)就可以,这样可以确保代码一致时反序列化成功:
添加一个随机生成的不重复的序列化 ID,如果反序列化前这个ID被修改了,反序列化时就会报错
添加 @SuppressWarnings
注解