Java 序列化与反序列化

概念

序列化是将 Java 对象转换成与平台无关的二进制流,而反序列化则是将二进制流恢复成原来的 Java 对象,二进制流便于保存到磁盘上或者在网络上传输。

原因

  • 永久性保存对象,保存对象的字节序列到本地文件或者数据库中
  • 通过序列化以字节流的形式使对象在网络中进行传递和接收
  • 通过序列化在进程间传递对象

实现方式

当序列化某个类的对象时,需要让该类实现 Serializable 接口或者 Externalizable 接口。

实现 Serializable 接口

如果实现 Serializable 接口,由于该接口只是个 “标记接口”,接口中不含任何方法,序列化是使用 ObjectOutputStream(处理流)中的 writeObject(obj) 方法将 Java 对象输出到输出流中,反序列化是使用 ObjectInputStream 中的 readObject(in) 方法将输入流中的 Java 对象还原出来。

序列化步骤
  1. 创建一个 ObjectOutputStream 输出流;
  2. 调用 ObjectOutputStream 对象的 writeObject() 输出可序列化对象。
反序列化步骤
  1. 创建一个 ObjectInputStream 输入流;
  2. 调用 ObjectInputStream 对象的 readObject() 得到序列化的对象。
自定义序列化

实现 Serializable 接口的类可以实现自定义序列化,只需要在类中提供下面这三个方法。

1
2
3
4
5
private void writeObject(java.io.ObjectOutStream out) throws IOException;

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;

private void readObjectNoData() throws ObjectStreamException;
  • 重写 writeObject()readObject() 方法,可以选择哪些属性需要序列化, 哪些属性不需要。如果writeObject() 使用某种规则序列化,则相应的readObject() 需要相反的规则反序列化,以便能正确反序列化出对象。
  • readObjectNoData() 方法是在序列化流不完整、序列化和反序列化版本不一致导致不能正确反序列时调用的容错方法。
  • writeReplace():在序列化时,会先调用此方法,再调用 writeObject() 方法。此方法可将任意对象代替目标序列化对象。
  • readResolve():反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃。此方法在 readeObject() 后调用。readResolve() 常用来反序列单例类,保证单例类的唯一性。

注意:readResolve()writeReplace() 的访问修饰符可以是private、protected、public,如果父类重写了这两个方法,子类都需要根据自身需求重写,这显然不是一个好的设计。通常建议对于 final 修饰的类重写 readResolve() 方法没有问题;否则,重写 readResolve() 使用 private 修饰。

示例代码
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
public class User implements Serializable {
private String name;
private int age;
private Date birthday;
private transient String gender;
private static final long serialVersionUID = -6849794470754667710L;

public User(String name,int age,Date date,String gender){
this.name=name;
this.age=age;
this.birthday=date;
this.gender=gender;
}

//省略 get\set 方法

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
", birthday=" + birthday +
'}';
}
}
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
public class SerializableDemo {
public static void main(String[] args) {
User user = new User("chris",22,new Date(),"male");
System.out.println(user.toString());

//Write Obj to File
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
}

//Read Obj from File
File file = new File("tempFile");
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
User newUser = (User) ois.readObject();
System.out.println(newUser);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
1
2
3
//输出结果,注意 gender =null 是 因为被 transient 修饰
User{name='chris', age=22, gender=male, birthday=Tue Dec 03 21:48:48 CST 2019}
User{name='chris', age=22, gender=null, birthday=Tue Dec 03 21:48:48 CST 2019}

实现 Externalizable 接口

如果实现 Externalizable接口,该接口继承自 Serializable 接口,在 Java Bean 类中实现接口中的writeExternal(out)readExternal(in) 方法,需要注意的是必须提供默认的无参构造函数,因为在反序列化的时候需要反射创建对象,否则反序列化失败。可以在序列化时选择如何序列化,比如对某个属性加密处理。

示例代码
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
public class Person implements Externalizable {

private String name;
private static int age=1000;
private transient String gender;
private Date date;

// 必须提供无参构造方法
public Person(){

}

//省略 get\set 方法

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
out.writeObject(gender);
out.writeObject(date);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = in.readObject().toString();
this.age = in.readInt();
this.gender=in.readObject().toString();
this.date=(Date) in.readObject();
}

@Override
public String toString(){
return "Name:"+this.name
+" Age:"+this.age
+" Sex:"+this.gender
+" Date"+this.date;
}
}
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
public class ExternalizableDemo {
public static void main(String[] args) throws IOException {
Person person=new Person();
person.setName("dawson");
person.setAge(22);
person.setDate(new Date());
person.setGender("male");
System.out.println("开始:"+person.toString());

File file=new File("hello.txt");
ObjectOutput objectOutput=new ObjectOutputStream(new FileOutputStream(file));
ObjectInput objectInput=new ObjectInputStream(new FileInputStream(file));
try {
person.writeExternal(objectOutput);
}catch (IOException e){
e.printStackTrace();
}

Person newPerson=new Person();
try {
newPerson.readExternal(objectInput);
System.out.println("结束:"+newPerson.toString());
}catch (ClassNotFoundException e){
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
}
}
}
1
2
3
//输出结果
开始:Name:dawson Age:22 Sex:male DateTue Dec 03 21:53:12 CST 2019
结束:Name:dawson Age:22 Sex:male DateTue Dec 03 21:53:12 CST 2019

两种序列化方式对比

Serializable接口 Externalizable接口
系统自动存储必要的信息 自定义存储信息
Java默认支持,易于实现,只需实现接口即可 需要实现两个方法

版本问题

反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢?Java序列化提供了一个 private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化。

如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报 InvalidClassException 异常。

如果不显式定义这个 SerialVersionUID,Java 虚拟机会根据类的信息自动生成,修改前和修改后的计算结果往往不同,造成版本不兼容而发生反序列化失败,另外由于平台的差异性,在程序移植中也可能出现无法反序列化。

强大的 IDE 工具,也都有自动生成 SeriaVersionUID 的方法。JDK 中自带的也有生成 SeriaVersionUID 值的工具 serialver.exe,使用 serialver 类名(编译后) 命令就能生成该类的 SeriaVersionUID 值。

什么情况下需要修改 serialVersionUID ?

  1. 如果只是修改方法,反序列化不受影响,则无需修改版本号;
  2. 如果只是修改静态变量,以及 transient 修饰的变量,反序列化不受影响,无需修改版本号;
  3. 如果修改非静态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改 serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

序列化算法

所有保存到磁盘的对象都有一个序列化编码号,当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。如果此对象已经序列化过,则直接输出编号即可。

由于序利化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。

ArrayList 序列化

首先看一下 ArrayList 源码,如下:

1
2
3
4
5
6
7
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{

private static final long serialVersionUID = 8683452581122892189L;
transient Object[] elementData; // non-private to simplify nested class access
private int size;
}

ArrayList 实现了 java.io.Serializable 接口,可以进行序列化及反序列化。从源码中可知 elementData 是 transient 的,是无法通过序列化进行持久化的,但是事实上 ArrayList 是可以序列化的,原因如下:

在 ArrayList 中定义了来个方法 writeObject()readObject()

在序列化过程中,如果被序列化的类中定义了 writeObjectreadObject 方法,虚拟机会试图调用对象类里的 writeObjectreadObject 方法,进行用户自定义的序列化和反序列化。
如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。
用户自定义的 writeObjectreadObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

为什么 ArrayList 要用这种方式来实现序列化呢?

ArrayList 实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化 99 个 null 元素。为了保证在序列化的时候不会将这么多 null 同时进行序列化,ArrayList 把元素数组设置为 transient。

因此为了防止一个包含大量空对象的数组被序列化,为了优化存储,所以,ArrayList 使用 transient 来声明elementData。 但是,作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化下来,所以,通过重写 writeObjectreadObject 方法的方式把其中的元素保留下来。

  • writeObject 方法把 elementData 数组中的元素遍历的保存到输出流(ObjectOutputStream)中。
  • readObject 方法从输入流(ObjectInputStream)中读出对象并保存赋值到 elementData 数组中。

虽然 ArrayList 中写了 writeObjectreadObject 方法,但是这两个方法并没有显示被调用(两个方法都是 private 的)。那么如果一个类中包含 writeObjectreadObject 方法,那么这两个方法是怎么被调用的呢?

ObjectOutputStream 会调用这个类的 writeObject() 方法进行序列化,ObjectInputStream 会调用相应的 readObject() 方法进行反序列化。

那么 ObjectOutputStream 又是如何知道一个类是否实现了 writeObject() 方法呢?又是如何自动调用该类的 writeObject() 方法呢?答案是通过反射机制实现的。

接下来具体分析一下 ArrayList 中的 writeObjectreadObject 方法到底是如何被调用的呢?
为了节省篇幅,这里给出 ObjectOutputStream 的 writeObject 的调用栈:
writeObject —> writeObject0 —>writeOrdinaryObject—>writeSerialData—>invokeWriteObject
这里看一下 invokeWriteObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void invokeWriteObject(Object obj, ObjectOutputStream out)
throws IOException, UnsupportedOperationException
{
if (writeObjectMethod != null) {
try {
writeObjectMethod.invoke(obj, new Object[]{ out });
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof IOException) {
throw (IOException) th;
} else {
throwMiscException(th);
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}

其中 writeObjectMethod.invoke(obj, new Object[]{ out }); 是关键,通过反射的方式调用 writeObjectMethod 方法。

Serializable 明明就是一个空的接口,它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化的呢?

在进行序列化操作时,会判断要被序列化的类是否是 EnumArraySerializable 类型,如果不是则直接抛出 java.io.NotSerializableException

具体看一下 ObjectOutputStream 的 writeObject 的调用栈:

writeObject —> writeObject0 —>writeOrdinaryObject—>writeSerialData—>invokeWriteObject

writeObject0 方法中有这么一段代码,从中可以看出具体如何判断类是否可以实现序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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());
}
}

注意事项

  1. 需要注意的是反序列化读取的仅仅是 Java 对象中的数据,而不是包含 Java 类的信息,所以在反序列化时还需要对象所属类的字节码(class)文件,否则会出现 ClassNotFoundException 异常。
  2. 当一个父类实现序列化,子类自动实现序列化,不需要显式实现 Serializable 接口。
  3. 如果子类实现 Serializable 接口而父类未实现时,父类不会被序列化,但此时父类必须有个无参构造方法,否则会抛 InvalidClassException 异常。要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。
  4. 对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。
  5. 注意序列化属性的顺序要和属性反序列化中的顺序一样,否则在反序列化时不能恢复出原来的对象。
  6. 如果一个可序列化的类的成员不是基本类型,也不是 String 类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。
  7. Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
  8. 单例类序列化,需要重写 readResolve() 方法;否则会破坏单例原则。

Java 序列化的方式

Java 原生序列化

缺点:

  1. 无法跨语言:Java 语言序列化的字节数组,其他语言无法进行反序列化。
  2. 序列化后码流太大。
  3. 序列化性能太差:采用同步阻塞 IO,效率差。

Kryo

Kryo 是一个快速有效的 Java 二进制对象序列化框架。具有更高效、序列化之后字节数据更小、更易用等特点。应用场景有对象存入文件、数据库,或者在网络中传输。

Kryo的 Output 和 Input 类,也是一个装饰器类,可以内置 Java IO 的InputStream 和 OutputStream,也可以实现网络传输和存入文件。Kryo 广泛用在 Rpc 框架中,如 Dubbo 框架。

  • Rpc 框架比较关注的是性能,扩展性,通用性,Kryo 的性能与其他几种序列化方式对比中表现较好;
  • Kryo 的 API 比较友好;
  • 不过,Kryo 兼容性不是很好,使用时应注意序列化和反序列化两边的类结构是否一致;
  • Kryo 序列化时,不需要对象实现 Serializable。

Hessian

Hessian 是一个基于 HTTP 的高性能 RPC 框架,其序列化算法叫 Hessian 协议,是业界公认的一种高效率高压缩比的序列化方式,如:Dubbo 框架就支持 Hessian 序列化方式。

  • Hessian 序列化后的数据要比 Kryo 序列化后的数据大,但要比 Java 原生序列化方式好很多;
  • Hessian 跨语言支持比较好;
  • Hessian 需要实体类实现 Serializable 接口

XML

XML 是一种很常见的数据保存方式,XML具有优秀的跨平台、可读性好的特点,可用于构建基本的 Web Services 平台,不同于 RPC 框架,Web Services 是基于 HTTP 协议的,通过 SOAP 协议,使运行在不同的操作系统并使用不同的技术和编程语言的应用程序可以互相进行通信。SOAP 是基于 XML 为序列化和反序列化协议的结构化消息传递协议。Web Services 还使用网络服务描述语言—WSDL(Web Services Description Language),用于描述 Web Services 以及如何访问 Web Services,WSDL 基于 XML 语言格式。Web Services 使用 XML 来编解码数据,并使用 SOAP 来传输数据。

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
44
45
public class XmlSerializable {
public static void main(String[] args) throws Exception {

XmlSerializable serializable=new XmlSerializable();
serializable.WriteObject();
serializable.ReadObject();
}

public void WriteObject() throws IOException {
User user=new User();
user.setAge(88);
user.setName("KKo");

XStream xStream=new XStream();
xStream.alias("User",User.class);
try {

FileOutputStream fileOutputStream=new FileOutputStream("hello");
// ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);
// objectOutputStream.writeObject(user);
xStream.toXML(user,fileOutputStream);
}catch (Exception e){
e.printStackTrace();
}

}

public void ReadObject() throws Exception{
User user=null;
FileInputStream fileInputStream=new FileInputStream("hello");
// ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
// user=(User)objectInputStream.readObject();
XStream xStream=new XStream();
xStream.alias("User",User.class);
try {
user=(User) xStream.fromXML(fileInputStream);

}catch (Exception e){
e.printStackTrace();
}


System.out.println(user);
}
}

Json

JSON( JavaScript Object Notation )是一种轻量级的数据交换格式,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。使用最多的场景是用于 Web 服务和客户端浏览器之间进行数据交换,如:前端使用 Ajax 以 Json 格式向服务端发起请求,服务端以 Json 格式响应给客户端,客户端根据 Json 数据格式解析响应内容。当然,在网络中传输仍然需要转化成字节,不过很多语言都提供类包支持将 JSON 串转化成字节流,(注:JSON 串相当于一个满足 JSON 数据格式的字符串),如 Java 的 FastJsonJacksonGson,JavaScript 的 eval() 函数等。还有一些 Nosql 数据库、消息队列也支持 Json 序列化方式,如 Redis 存储对象时,使用 JSON 格式,使数据支持跨平台、可读性也更强。

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
public class JsonSerializable {
public static void main(String[] args) throws Exception{

User user=new User("James",444,null,"male");
System.out.println("序列化前:"+user);

try {
ObjectMapper objectMapper=new ObjectMapper();
byte[] bytes=null;
for (int i = 0; i < 10; i++) {
bytes=objectMapper.writeValueAsBytes(user);
}
User users1=new User();
users1=objectMapper.readValue(bytes,User.class);
System.out.println("序列化后:"+user);
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (JsonGenerationException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

}

ProtoBuff

ProtoBuff 是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化。适合做数据存储或 RPC 数据交换格式,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

优点:

  1. 跨语言
  2. 序列化后数据占用空间比 Json 小,Json 有一定的格式,在数据量上还有可以压缩的空间。

缺点:

  1. 以二进制的方式存储,无法直接读取编辑,除非你有 .proto 定义,否则无法直接读出内容。

Protostuff 是一个基于 ProtoBuff 实现的序列化方法,它较于 ProtoBuff 最明显的好处是,在几乎不损耗性能的情况下做到了不用我们写 .proto 文件来实现序列化。

与 Thrift 的对比:

  • 两者语法类似,都支持版本向后兼容和向前兼容。
  • Thrift 侧重点是构建跨语言的可伸缩的服务,支持的语言多,同时提供了全套 RPC 解决方案,可以很方便的直接构建服务,不需要做太多其他的工作。
  • ProtoBuff 主要是一种序列化机制,在数据序列化上进行性能比较,ProtoBuff 相对较好。

Thrift

目前流行的服务调用方式有很多种,例如基于 SOAP 消息格式的 Web Service,基于 JSON 消息格式的 RESTful 服务等。其中所用到的数据传输方式包括 XML,JSON 等,然而 XML 相对体积太大,传输效率低,JSON 体积较小,新颖,但还不够完善。

Thrift 是由 Facebook 开发的远程服务调用框架 Apache Thrift,它采用接口描述语言定义并创建服务,支持可扩展的跨语言服务开发,所包含的代码生成引擎可以在多种语言中,如 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk 等创建高效的、无缝的服务,其传输数据采用二进制格式,相对 XML 和 JSON 体积更小,对于高并发、大数据量和多语言的环境更有优势。

参考

  1. Java基础-序列化与反序列化
  2. 深入分析Java的序列化与反序列化
  3. java序列化,看这篇就够了
  4. 简述几种序列化方式