详解Java的序列化和反序列化
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/09/ )。
1. 概述
序列化:把Java对象转换为字节序列的过程。
反序列化:把字节序列恢复为Java对象的过程。
对象的序列化主要有两种用途:
- 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;(持久化对象)
- 在网络上传送对象的字节序列。(网络传输对象)
2. 使用
要想实现序列化,必须要实现下面两个接口之一:
- Serializable :
- 一个对象想要被序列化,那么它的类就要实现此接口
- 这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。
- 不想序列化的字段可以使用transient修饰。
- 由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。
- 使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。
- Externalizable :
- 他是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化
- 方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。
- 对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。
使用时,你只想隐藏一个属性,比如用户对象user的密码pwd,如果使用Externalizable,并除了pwd之外的每个属性都写在writeExternal()方法里,这样显得麻烦,可以使用Serializable接口,并在要隐藏的属性pwd前面加上transient就可以实现了。如果要定义很多的特殊处理,就可以使用Externalizable。
当然这里我们有一些疑惑,Serializable 中的writeObject()方法与readObject()方法科可以实现自定义序列化,而Externalizable 中的writeExternal()和readExternal() 方法也可以,他们有什么异同呢? * readExternal(),writeExternal()两个方法,这两个方法除了方法签名和readObject(),writeObject()两个方法的方法签名不同之外,其方法体完全一样。 * 需要指出的是,当使用Externalizable机制反序列化该对象时,程序会使用public的无参构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参构造。 * 虽然实现Externalizable接口能带来一定的性能提升,但由于实现ExternaLizable接口导致了编程复杂度的增加,所以大部分时候都是采用实现Serializable接口方式来实现序列化。
- 序列化版本
在序列化过程中,可以控制序列化的版本。该字段为被序列化对象中的serialVersionUID字段。
public class User implements Serializable {
private static final long serialVerisionUID = 1L ;
...
}
一个对象数据,在反序列化过程中,如果序列化串中的serialVersionUID与当前对象值不同,则反序列化失败,否则成功。
如果serialVersionUID没有显式生成,系统就会自动生成一个。生成的输入有:类名、类及其属性修饰符、接口及接口顺序、属性、静态初始化、构造器。任何一项的改变都会导致serialVersionUID变化。
属性的变化都会导致自动生成的serialVersionUID发生变化。例如,对于对象A,我们生成序列化的S(A),然后修改A的属性,则此时A的serialVersionUID发生变化。反序列化时,S(A)与A的serialVersionUID不同,无法反序列化。会报序列号版本不一致的错误。
为了避免这种问题, 一般系统都会要求实现serialiable接口的类显式的生明一个serialVersionUID。显式定义serialVersionUID的两种用途: * 希望类的不同版本对序列化兼容时,需要确保类的不同版本具有相同的serialVersionUID; * 不希望类的不同版本对序列化兼容时,需要确保类的不同版本具有不同的serialVersionUID。
如果我们保持了serialVersionUID的一致,则在反序列化时,对于新增的字段会填入默认值null(int的默认值0),对于减少的字段则直接忽略。
- 使用举例
我们假设有一个对象需要进行序列化:
public class User implements Serializable {
private static final long serialVersionUID = 25454531454L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后,可以通过以下方式进行序列化和反序列化:
public class Main {
public static void main(String[] args) {
private static String FILE_NAME = "D:/userData.bin";
//序列化
User user = new User();
user.setName("xiaoming");
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_NAME));
oos.writeObject(user);
oos.close();
} catch (Exception e) {
e.printStackTrace();
}
//反序列化
User newUser = null;
try {
ObjectInput input = new ObjectInputStream(new FileInputStream(FILE_NAME));
newUser = (User)input.readObject();
input.close();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("name = " + newUser.getName());
}
}
这样,我们将对象user的信息序列化后写入到二进制文件中,并读出来之后又反序列化为newUser对象。
关于序列化的使用,推荐大家阅读MyBatis的源码。为什么呢?因为MyBaits作为一个ORM框架,需要频繁进行内存中对象和数据库中序列化字符串的转换。在这方面,用的很到位。
这方面,推荐一本书《通用源码阅读指导书》。这是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,非常推荐。
在书中的第5章介绍了序列化的基本知识和异常信息的序列化、第19章介绍了缓存中的序列化装饰器、第22章介绍了序列化中的钩子方法和MyBatis懒加载功能中的序列化。非常棒!
多读读源码,进步会很大。
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。