是 Java 提供的序列化接口,它是一个空接口:
Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。
Serializable 入门
Serializable 有以下几个特点:
- 可序列化类中,未实现 Serializable 的属性状态无法被序列化/反序列化
- 也就是说,反序列化一个类的过程中,它的非可序列化的属性将会调用无参构造函数重新创建
- 因此这个属性的无参构造函数必须可以访问,否者运行时会报错
- 一个实现序列化的类,它的子类也是可序列化的
序列化与反序列化 Serializable
Serializable 的序列化与反序列化分别通过 ObjectOutputStream 和 ObjectInputStream 进行
Java 的序列化步骤与数据结构分析
序列化算法一般会按步骤做如下事情:
- 将对象实例相关的类元数据输出。
- 递归地输出类的超类描述直到不再有超类。
- 类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
- 从上至下递归输出实例的数据
格式化后以二进制打开
- AC ED: STREAM_MAGIC. 声明使用了序列化协议.
- 00 05: STREAM_VERSION. 序列化协议版本.
- 0x73: TC_OBJECT. 声明这是一个新的对象.
- 0x72: TC_CLASSDESC. 声明这里开始一个新 Class。
- 00 2e: Class 名字的长度.
readObject/writeObject 原理分析
writeObject
-
ObjectOutputStream 的构造函数设置 enableOverride = false
以oos.writeObject(obj)为例分析
-
所以 writeObject 方法执行的是 writeObject0(obj, false);
-
在 writeObject0 方法中,代码非常多,看重点
-
在 writeOrdinaryObject(obj, desc, unshared)方法中
writeSerialData 方法,主要执行方法:defaultWriteFields(obj, slotDesc)
-
在 ObjectStreamClass 中,ObjectOutputStream(ObjectInputStream)会寻找目标类中的私有的 writeObject(readObject)方法,赋值给变量 writeObjectMethod(readObjectMethod)
Serializable序列化
- 序列化(serialize) - 序列化是将对象转换为字节流。
- 反序列化(deserialize) - 反序列化是将字节流转换为对象。
- 序列化用途
- 序列化可以将对象的字节序列持久化——保存在内存、文件、数据库中。
- 在网络上传送对象的字节序列。
- RMI(远程方法调用)
注意:使用 Java 对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。
序列化和反序列化
Java 通过对象输入输出流来实现序列化和反序列化:
-
java.io.ObjectOutputStream
类的writeObject()
方法可以实现序列化; -
java.io.ObjectInputStream
类的readObject()
方法用于实现反序列化。
序列化和反序列化示例:
// Gender类,表示性别
// 每个枚举类型都会默认继承类java.lang.Enum,而Enum类实现了Serializable接口,所以枚举类型对象都是默认可以被序列化的。
public enum Gender {
MALE, FEMALE
}
// Person 类实现了 Serializable 接口,它包含三个字段。另外,它还重写了该类的 toString() 方法,以方便打印 Person 实例中的内容。
public class Person implements Serializable {
private String name = null;
private Integer age = null;
private Gender gender = null;
public Person() {
System.out.println("none-arg constructor");
}
public Person(String name, Integer age, Gender gender) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
this.gender = gender;
}
// 省略 set get 方法
@Override
public String toString() {
return "[" + name + ", " + age + ", " + gender + "]";
}
}
// SimpleSerial类,是一个简单的序列化程序,它先将Person对象保存到文件person.out中,然后再从该文件中读出被存储的Person对象,并打印该对象。
public class SimpleSerial {
public static void main(String[] args) throws Exception {
File file = new File("person.out");
ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file)); // 注意这里使用的是 ObjectOutputStream 对象输出流封装其他的输出流
Person person = new Person("John", 101, Gender.MALE);
oout.writeObject(person);
oout.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file)); // 使用对象输入流读取序列化的对象
Object newPerson = oin.readObject(); // 没有强制转换到Person类型
oin.close();
System.out.println(newPerson);
}
}
// 上述程序的输出的结果为:
arg constructor
[John, 31, MALE]
当重新读取被保存的Person对象时,并没有调用Person的任何构造器,看起来就像是直接使用字节将Person对象还原出来的。
当Person对象被保存到person.out文件后,可以在其它地方去读取该文件以还原对象,但必须确保该读取程序的 CLASSPATH 中包含有 Person.class(哪怕在读取Person对象时并没有显示地使用Person类,如上例所示),否则会抛出 ClassNotFoundException。
简单的来说,Java 对象序列化就是把对象写入到输出流中,用来存储或传输;反序列化就是从输入流中读取对象。
序列化一个对象首先要创造某些OutputStream对象(如FileOutputStream、ByteArrayOutputStream等),然后将其封装在一个ObjectOutputStream对象中,在调用writeObject()方法即可序列化一个对象
反序列化的过程需要创造InputStream对象(如FileInputstream、ByteArrayInputStream等),然后将其封装在ObjectInputStream中,在调用readObject()即可
为什么一个类实现了Serializable接口,它就可以被序列化?
被序列化的类必须属于Enum
、Array
和 Serializable
类型其中的任何一种,否则将抛出 NotSerializableException
异常。这是因为:在序列化操作过程中会对类型进行检查,如果不满足序列化类型要求,就会抛出异常。
使用ObjectOutputStream来持久化对象到文件中,使用了writeObject方法,该方法又调用了如下方法:
private void writeObject0(Object obj, boolean unshared) throws IOException {
...
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());
}
}
...
}
从上述代码可知,如果被写对象的类型是String,或数组,或Enum,或Serializable,那么就可以对该对象进行序列化,否则将抛出NotSerializableException。
即、String类型的对象、枚举类型的对象、数组对象,都是默认可以被序列化的。
默认序列化机制
如果仅仅让某个类实现Serializable接口,而没有其它任何处理的话,则就是使用默认序列化机制。
使用默认机制在序列化对象时,不仅会序列化当前对象,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。
所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。
选择性的序列化
在现实应用中,有些时候不能使用默认序列化机制。比如,希望在序列化过程中忽略掉敏感数据,或者简化序列化过程。下面将介绍若干影响序列化的方法。
使用 transient 关键字
当类的某个字段被 transient 修饰,默认序列化机制就会忽略该字段。此处将Person类中的age字段声明为transient,如下所示
public class Person implements Serializable {
...
transient private Integer age = null;
...
}
// 再执行SimpleSerial应用程序,会有如下输出:
arg constructor
[John, null, MALE]
使用writeObject()方法与readObject()方法
public class Person implements Serializable {
...
transient private Integer age = null;
...
// writeObject()会先调用ObjectOutputStream中的defaultWriteObject()方法,该方法会执行默认的序列化机制,此时会忽略掉age字段。然后再调用writeInt()方法显示地将age字段写入到
// ObjectOutputStream中。
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}
// readObject()的作用则是针对对象的读取,其原理与writeObject()方法相同。
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
}
// 再次执行SimpleSerial应用程序,则又会有如下输出:
arg constructor
[John, 31, MALE]
必须注意地是,writeObject()与readObject()都是private方法,那么它们是如何被调用的呢?
毫无疑问,使用反射。详情可以看看ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。这两个方法会在序列化、反序列化的过程中被自动调用。且不能关闭流,否则会导致序列化操作失败。
使用 Externalizable 接口
无论是使用 transient 关键字,还是使用 writeObject() 和 readObject() 方法,其实都是基于 Serializable 接口的序列化。
Java提供了另一个序列化接口 Externalizable,使用该接口之后,之前基于 Serializable 接口的序列化机制就将失效。
Externalizable 接口继承于 Serializable 接口,当使用该接口时,序列化的细节需要由程序员去完成。将Person类作如下修改:
public class Person implements Externalizable {
private String name = null;
transient private Integer age = null;
private Gender gender = null;
public Person() {
System.out.println("none-arg constructor");
}
public Person(String name, Integer age, Gender gender) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
this.gender = gender;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
...
}
// 此时再执行SimpleSerial程序,会得到如下结果:
arg constructor
none-arg constructor
[null, null, null]
// 从该结果,一方面可以看出Person对象中任何一个字段都没有被序列化。另一方面,这次序列化过程调用了Person类的无参构造器。
Externalizable 继承于 Serializable,当使用该接口时,序列化的细节需要由程序员去完成。
如上所示的代码,由于实现的writeExternal()与readExternal()方法未作任何处理,那么该序列化行为将不会保存/读取任何一个字段。这也就是为什么输出结果中所有字段的值均为空。
另外,使用 Externalizable 接口进行序列化时,读取对象会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中,这就是为什么在此次序列化过程中Person类的无参构造器会被调用。由于这个原因,实现 Externalizable 接口的类必须要提供一个无参构造器,且它的访问权限为public。
对上述Person类做进一步的修改,使其能够对name与age字段进行序列化,但忽略 gender 字段:
public class Person implements Externalizable {
private String name = null;
transient private Integer age = null;
private Gender gender = null;
public Person() {
System.out.println("none-arg constructor");
}
public Person(String name, Integer age, Gender gender) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
this.gender = gender;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
...
}
// 执行SimpleSerial之后会有如下结果:
arg constructor
none-arg constructor
[John, 31, null]
readResolve()方法——单例模式的反序列化
当使用Singleton模式时,应该是期望某个类的实例应该是唯一的,但如果该类是可序列化的,那么情况可能略有不同。当然目前最好的单例实现方式是使用枚举,如果还是传统的实现方式,才会遇到这个问题。
serialVersionUID
serialVersionUID 有什么作用,如何使用 serialVersionUID?
serialVersionUID 是 Java 为每个序列化类产生的版本标识。它可以用来保证在反序列时,发送方发送的和接受方接收的是可兼容的对象。如果接收方接收的类的 serialVersionUID 与发送方发送的 serialVersionUID 不一致,会抛出 InvalidClassException。
如果可序列化类没有显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值。尽管这样,还是建议在每一个序列化的类中显式指定 serialVersionUID 的值。因为不同的 jdk 编译很可能会生成不同的 serialVersionUID 默认值,从而导致在反序列化时抛出 InvalidClassExceptions 异常。
serialVersionUID 字段必须是 static final long
类型。
我们来举个例子:
(1)有一个可序列化类 Person
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
private String address;
// 构造方法、get、set 方法略
}
(2)开发过程中,对 Person 做了修改,增加了一个字段 email,如下:
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
private String address;
private String email;
// 构造方法、get、set 方法略
}
由于这个类和老版本不兼容,我们需要修改版本号:
private static final long serialVersionUID = 2L;
再次进行反序列化,则会抛出 InvalidClassException 异常。
综上所述,我们大概可以清楚:serialVersionUID 用于控制序列化版本是否兼容。若我们认为修改的可序列化类是向后兼容的,则不修改 serialVersionUID。
序列化和反序列化需要注意的坑
能序列化的前提
如果一个类想被序列化,需要实现 Serializable 接口进行自动序列化,或者实现 Externalizable 接口进行手动序列化,否则强行序列化该类的对象,就会抛出 NotSerializableException 异常,这是因为,在序列化操作过程中会对类型进行检查,要求被序列化的类必须属于 Enum、Array 和 Serializable 类型其中的任何一种(Externalizable也继承了Serializable)。
JVM 是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)
transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
FileOutputStream 类有一个带有两个参数的重载 Constructor——FileOutputStream(String, boolean)。若其第二个参数为 true 且 String 代表的文件存在,那么将把新的内容写到原来文件的末尾而非重写这个文件,故不能用这个版本的构造函数来实现序列化,也就是说必须重写这个文件,否则在读取这个文件反序列化的过程中就会抛出异常,导致只有第一次写到这个文件中的对象可以被反序列化,之后程序就会出错。
要知道序列化的是什么样儿的对象(成员)
序列化并不保存静态变量
要想将父类对象也序列化,就需要让父类也实现 Serializable 接口
若一个类的字段有引用对象,那么在序列化该类的时候不仅该类要实现Serializable接口,这个引用类型也要实现Serializable接口。但有时我们并不需要对这个引用类型进行序列化,此时就需要使用transient关键字来修饰该引用类型保证在序列化的过程中跳过该引用类型。
通过序列化操作,可以实现对任何可 Serializable 对象的深度复制(deep copy),这意味着复制的是整个对象的关系网,而不仅仅是基本对象及其引用
如果父类没有实现Serializable接口,但其子类实现了此接口,那么这个子类是可以序列化的,但是在反序列化的过程中会调用父类的无参构造函数,所以在其直接父类(注意是直接父类)中必须有一个无参的构造函数。
序列化的安全性
服务器端给客户端发送序列化对象数据,序列化二进制格式的数据写在文档中,并且完全可逆。
一抓包就能就看到类是什么样子,以及它包含什么内容。如果对象中有一些数据是敏感的,比如密码字符串等,则要对字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
比如可以通过使用 writeObject 和 readObject 实现密码加密和签名管理,但其实还有更好的方式。
如果需要对整个对象进行加密和签名,最简单的是将它放在一个 javax.crypto.SealedObject 和/或 java.security.SignedObject 包装器中。两者都是可序列化的,所以将对象包装在 SealedObject 中可以围绕原对象创建一种 “包装盒”。必须有对称密钥才能解密,而且密钥必须单独管理。同样,也可以将 SignedObject 用于数据验证,并且对称密钥也必须单独管理
反序列化后,何时不是同一个对象
只要将对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,而且只要在同一流中,对象都是同一个。
否则,反序列化后的对象地址和原对象地址不同,只是内容相同
如果将一个对象序列化入某文件,那么之后又对这个对象进行修改,然后再把修改的对象重新写入该文件,那么修改无效,文件保存的序列化的对象仍然是最原始的。这是因为,序列化输出过程跟踪了写入流的对象,而试图将同一个对象写入流时,并不会导致该对象被复制,而只是将一个句柄写入流,该句柄指向流中相同对象的第一个对象出现的位置。为了避免这种情况,在后续的 writeObject() 之前调用 out.reset() 方法,这个方法的作用是清除流中保存的写入对象的记录
ArrayList 序列化要注意的问题
ArrayList实现了java.io.Serializable接口,但是其 elementData 是 transient 的,但是 ArrayList 是通过数组实现的,数组 elementData 用来保存列表中的元素。通过该属性的声明方式知道该数据无法通过序列化持久化。
但是如果实际测试,就会发现,ArrayList 能被完整的序列化,原因是在writeObject 和 readObject方法中进行了序列化的实现。
这样设计的原因是因为 ArrayList 是动态数组,如果数组自动增长长度设为 2000,而实际只放了一个元素,那就会序列化 1999 个 null 元素,为了保证在序列化的时候不会将这么多 null 元素序列化,ArrayList 把元素数组设置为transient,但是,作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化,所以,通过重写 writeObject 和 readObject 方法把其中的元素保留下来,具体做法是:
writeObject方法把elementData数组中的元素遍历到ObjectOutputStream
readObject方法从ObjectInputStream中读出对象并保存赋值到elementData数组
序列化问题
Java 的序列化能保证对象状态的持久保存,但是遇到一些对象结构复杂的情况还是难以处理,这里归纳一下:
- 父类是 Serializable,所有子类都可以被序列化。
- 子类是 Serializable ,父类不是,则子类可以正确序列化,但父类的属性不会被序列化(不报错,数据丢失)。
- 如果序列化的属性是对象,则这个对象也必须是 Serializable ,否则报错。
- 反序列化时,如果对象的属性有修改或删减,则修改的部分属性会丢失,但不会报错。
- 反序列化时,如果 serialVersionUID 被修改,则反序列化会失败。
序列化技术选型
ava 官方的序列化存在许多问题,因此,建议使用第三方序列化工具来替代。
Java 官方的序列化主要体现在以下方面:
Java 官方的序列无法跨语言使用。
Java 官方的序列化性能不高,序列化后的数据相对于一些优秀的序列化的工具,还是要大不少,这大大影响存储和传输的效率。
Java 官方的序列化一定需要实现 Serializable 接口。
Java 官方的序列化需要关注 serialVersionUID。
当然我们还有更加优秀的一些序列化和反序列化的工具,根据不同的使用场景可以自行选择!hessian - 适用于对开发体验敏感,性能有要求。
jackson、gson、fastjson - 适用于对序列化后的数据要求有良好的可读性(转为 json 、xml 形式)。
#参考资料
参考
Java对象序列化全面总结
https://www.cnblogs.com/kubixuesheng/p/10344533.html
深入理解 Java 序列化
网络传输: 序列化与反序列化