公众号:追风栈Binary
我们通过文本或者数据库的方式来永久化存储程序中产生的数据。这种思路也可以借鉴到保存Java对象这类问题上来,因为Java对象的存在是临时性的,等到程序结束后就会被垃圾回收机制进行回收。所以需要特定的手段来完成相应的转换过程。
什么是Java对象的序列化和反序列化?
以一个POJO(Plain Ordinary Java Object,简单的Java对象)
的User
类为例,当我们创建了一个User
对象myUser
之后,这个myUser
对象就会存储在JVM
(Java虚拟机)上的堆内存中,只要垃圾回收机制没有盯上myUser
,那么在程序运行期间我们总能有方法来访问到这个对象所包含的属性以及方法。而当JVM
停止运行,那么myUser
就会被回收,我们就再也拿不到myUser
所包含的属性和方法了。
那问题是,如果我们需要保存这个myUser
对象的状态信息,并加以保存,并在以后有需要的情况下将它原封不动的加载回来。那这个过程,就是Java对象的序列化与反序列化。我自己将这个过程画了个图,如下:
从术语角度来说,
Serialization
是Java内置的一个API
,通过这个步骤,将对象状态信息
转化为字节流的方式,用于存储或者作为信息进行传输;而后再根据需要,通过反序列化的方式,将字节流转换成初始的对象。例如上图中类生成的对象小明,我们可以把他变成一种文本方式存储起来,等需要小明工作的时候,再将小明复原。这就是序列化和反序列的基本原理。
什么是Serializable?
Serializable
是Java io
包中的一个内置接口,这个接口非常的有意思,从上面的过程来看,要实现这个接口的功能应该很复杂才对,但实际上,我们通过IDEA打开它的源码,就会发现:
里面啥也没有,只是一个
Serializable
接口声明。实际上,这个接口它仅用作标识实现该接口的类可以被序列化。
在序列化对象的过程中,声明实现该接口是必须的,如果上图中的类不实现Serializable
这个接口,就会报出NotSerializationException
这个异常。
在使用前应该注意的几个问题
在实现序列化和反序列化的过程中,需要注意几个问题。
静态变量在序列化过程中并不会被保存:序列化保存的是对象状态,而静态变量是属于类变量,因此并不会被保存。
一个类能不能序列化,就看它是否实现
Serializable
这个接口。并且如果子类的父类实现了该接口,那么这个子类也可以实现序列化。serialVersionUID
:这是一个与可序列化的类相关联的版本号,在反序列化的过程中,这个序列号用来验证是不是该类进行的序列化过程。匹配成功,则进行反序列化,若serialVersionUID
匹配不成功,那么就会抛出InvalidClassException
这个异常。虽然可以不人为设定这个数值,但该接口的API
强烈建议开发者自行对这个版本号进行显式声明,并且规定这个值必须是static
类型和final
类型。通常一般我们也会将其设为priavte
私有变量的表述方式,例如:priavte static final long serialVersionUID = 1L
-
Transient
关键字控制的变量不会序列化到目标文件中,当反序列化完成后,这个Transient
修饰的变量将被赋予Java规定的初始值。例如在下图中,字符串gender
被赋予了默认值null
如何持久化一个对象:序列化与反序列化?
以第一段中的User
类为例,对这个User
类的对象进行序列化过程。User
类是一个简单的JaveBean
,它实现了序列化的接口Serializable
,意味着这个类可以序列化。其中gender
属性被transient
修饰,意味着这个变量将不会被序列化。除此之外,serialVersionUID
被设置为1L
,注意long
长整型的后面需要加L
修饰,并用大写字母L
修饰,不要用小写l
,避免与数字1发生混淆。
public class User implements Serializable {
private String name;
private int age;
private String work;
private transient String gender;
private String words;
private static final long serialVersionUID = 1L;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getWork() {
return work;
}
public void setWork(String work) {
this.work = work;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getWords() {
return words;
}
public void setWords(String words) {
this.words = words;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", work='" + work + '\'' +
", gender='" + gender + '\'' +
", words='" + words + '\'' +
'}';
}
}
在定义了User
类后,首先是利用该类创建一个对象小明。
User user = new User();
user.setName("小明");
user.setGender("male");
user.setAge(25);
user.setWork("Programmer");
user.setWords("hello");
然后是对象的序列化过程,在这个步骤中需要ObjectOutputStream
将对象的基本数据类型写入流中。ObjectOutputStream
很独特,它只能将支持Serializable
接口的对象写入字节流。默认的对象序列化机制写入字节流的内容包括:对象的类、类签名、非静态字段的值。现在我们需要把小明这个对象的状态写入文本中加以保存。
//将对象写入字节流
ObjectOutputStream objectOutputStream;
try{
// 将一个OutputStream对象作为构造函数的参数创建对象,这里是文件对象
objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));
// 将小明这个对象写入到这个文本中
objectOutputStream.writeObject(user);
//关闭流
objectOutputStream.close();
}catch (IOException e){
e.printStackTrace();
}
运行程序,就可以看到文件夹下生成了名为tempFile
的文件,这就完成了对小明这个对象序列化的过程。打开文件查看里面的内容,但是会发现我们并不明白这到底写的是什么,但是这个就是小明被序列化后的模样。
由于写入的是字节流,因此对阅读者并不友好。后续会用一种更为完善的方法来解决这种问题。
在序列化之后,我们又突然需要复原小明,这就必须进行对象的反序列化过程。反序列化过程与序列化过程类似,它需要ObjectInputStream
方法来读入需要复原的字节流,然后调用readObject
的方法转换成对象。在这个过程中,会对序列号serialVersionUID
进行校验工作,以确保类型匹配正确。
File file = new File("tempFile");
ObjectInputStream objectInputStream;
try{
// 将存储小明对象的文件流作为参数来创建一个对象
objectInputStream = new ObjectInputStream(new FileInputStream(file));
// 从这个字节流中读取对象,并强制转换为User类对象
User myUser = (User) objectInputStream.readObject();
System.out.println(myUser);
}catch (IOException e){
e.printStackTrace();
}catch (ClassNotFoundException e){
e.printStackTrace();
}
从控制台中查看复原的小明,看看他和我们序列化之前有什么区别:
可以看出,小明的gender
因为被transient
修饰,并没有被序列化,因此反序列化后字段被赋予了默认值null
,就这样,小明被反序列化为一个身份不明的人。
序列化和反序列化在实际应用中十分广泛,例如远程通信传输对象信息的时候,就需要将对象转换成网络所允许的二进制字节流。但是在上面我们也发现,保存到本地的对象文本由于编码问题,导致可读性很差,有没有什么更好的替代方案呢?
Json与FastJson库
Json是一种轻量级的数据交换格式,在实际的工作中,Json的使用非常的频繁。Json通过键值对来表示对象的属性和属性值。如下的Demo.json
展示了Json文件的一般形式。
Json文件的特点在于结构清晰,简明易读。并且在序列化与反序列化上,Json使用起来也更方便。在这个问题上,阿里巴巴提供了一个非常好用的工具FastJson,它是阿里巴巴开源的Json解析库,并且支持将JavaBean序列化为Json字符串,也可以从Json字符串反序列为JavaBean。
FastJson使用非常简单,如果不是使用Maven构建项目,那就需要下载FastJson的Jar包进行导入,在IDEA中按照如下步骤添加FastJson库
点击IDEA的
File
,弹出后选择Project Structure
在弹出的页面选择
Modules
,并在随后的页面中选择Dependencies
在
Dependencies
页面下点击右侧绿色的+
号,弹出的小菜单中选择JARs or directories
导入自己路径下的FastJson的Jar包,点击
Apply
后点击ok
测试FastJson库是否正常运行
使用FastJson对JavaBean进行序列化与反序列化简直不要太简单了,两个过程都只需要一行代码便可以完成相关操作。例如FastJson对小明进行序列化,见如下代码:
User user = new User();
user.setName("小明");
user.setGender("male");
user.setAge(25);
user.setWork("Programmer");
user.setWords("hello");
String jsonData = JSONObject.toJSONString(user);
System.out.println(jsonData);
通过JSONObject调用toJSONString
的方法,传入需要序列化的对象,只需要一行代码,就可以得到对象序列化之后的字符串。通过打印就可以看出这个对象里面到底有些啥。
依旧可以看出,被transient
修饰的gender
并没有被序列化,而这一次,我们很直观的看到了小明这个对象对序列化之后的结果。
那么反序列化过程也同样简洁:
User myUser = JSONObject.parseObject(jsonData, User.class);
System.out.println(myUser);
反序列化过程的parseObject
方法接受一个序列化后的字符串和目标对象的类,生成一个反序列化的对象后,通过User
声明的myUser
引用反序列化后的对象,打印这个对象,得到如下的结果:
注意这个对象的输出结果与上面序列化后的结果的差异,gender
反序列化后被赋予了null
,其实这也说明了反序列化过程按照类的模板进行完全匹配的,对于没能序列化的对象状态,反序列化都尽量来恢复对象的初始状态。
由此可以看出,Json文件和FastJson库在Java的序列化和反序列化的作用效率会更高,并且JavaBean不需要实现序列化Serializable
接口,直接就配合FastJson可以使用,大幅提升了编码质量。
总结
在实际工作场景中,序列化和反序列化是一个十分常见常用的功能,现如今大部分都在采纳Json和FastJson解析库结合的这种方式进行交互,但是对于Java中Serializable的基本性质还是需要了解。更深入的,对于Serializable中的writeObject
方法和readObject
方法实现自定义的序列化策略也还需要进一步的研究和实践,后续会及时更新。