一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
什么是序列化与反序列化
序列化:是一种将对象转换为可传输的或者可存储的数据格式的过程,序列化后可用于网络进行数据传输,也可用于数据持久化。但是一般常用于网络传输。
反序列化:就是序列化的逆操作。 序列化与反序列化之间的数据格式一般是二进制的字节码,有网友说还有xml格式,但是没见过,也没用过,就暂不做讨论。
Java中的序列化与反序列化
Java是一门面向对象的编程语言,所以Java中的序列化与反序列化也就可以看成是对对象的持久化,以及网络传输,因为对象里面的信息就是数据。
有哪些应用?🤔
1、对象存在Java虚拟机的堆内存中,Java虚拟机又存在于电脑的内存条里,一旦当前运行的Java虚拟机进程被干掉,那么所谓的对象也就不存在了。所以运用Java的序列化机制,我们就可以在对象生命周期结束之前,也就是被jvm回收之前将对象持久化到操作系统的文件系统中,也可以持久化到数据库(但是究其根本都是存到磁盘,具体的视需求、场景而定),这样在下一次启动Java程序启动时,或者某一对象被销毁后,仍然可以从文件中将对象通过反序列化操作还原对象(当然前提是得事先持久化存储)。
2、用于对象的深度克隆(clone)操作。
3、网络传输,也就是跨jvm之间通过网络传递Object序列化后者的二进制字节流来达到传递对象数据的作用。
前提:实现Serializable接口
想要使得对象能支持序列化与反序列化,就必须实现java.io.Serializable接口,这是一个标识性接口,是给编译器看的,里面没有任何内容。
但是对没有实现这个接口的类对象进行实例化,会抛出NotSerializableException异常。
why?因为在new ObjectOutputStream(new FileOutputStream(objFile)).writeObject(obj)里面的底层实现writeObject0方法中有关键的判断语句:如果不是String、数组、Enum、Serializable就会抛出上述异常,前面的逻辑虽然看不太明白,但是看注释的关键词都是什么替换、检查什么的,应该是与我们要探究的无关。也可以dubug来追踪。
code 1:
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
...
// remaining cases
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());
}
}
...
}
transient关键字
作用:这个关键字用于修饰属性,作用是修饰的属性将不会参与序列化与反序列化,即在序列化时忽略的属性。
用途:对于某些无关紧要的数据可忽略,能节省磁盘空间,或者减少网络与不必要的性能开销。
序列化与反序列化的简单使用
code 2:
package transientstudy;
import org.junit.Test;
import java.io.*;
/**
* description 测试程序,序列化之再进行反序列化
* @author shiPengYu
* @date 2022/4/17
*/
public class TransientStudy {
@Test
public void serialization() throws Exception{
// 序列化
File objFile = new File("./src/transientstudy/obj.txt");
if (!objFile.exists()){
objFile.createNewFile();
}
ObjectOutput oo = new ObjectOutputStream(new FileOutputStream(objFile));
People people = new People("张三","男",25);
System.out.println("序列化: "+people.toString());
oo.writeObject(people);
oo.close();
}
@Test
public void deserialize() throws Exception{
// 反序列化
File objFile = new File("./src/transientstudy/obj.txt");
ObjectInput oi = new ObjectInputStream(new FileInputStream(objFile));
People people1 = (People) oi.readObject();
System.out.println("反序列化: "+people1.toString());
// 释放
oi.close();
}
}
class People implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String sex;
private transient int age;
public People(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
public People() {
}
@Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
'}';
}
}
测试结果
age字段值被忽略参与序列化,由构造函数赋予了初始值0
需要注意的点
1、不包括方法
序列化只是针对属性,不包括方法
2、serialVersionUID须一致
序列化与反序列化的序列化ID,也就是serialVersionUID常量必须保持一致,否则会反序列化失败。
code 3:
package transientstudy;
import org.junit.Test;
import java.io.*;
/**
* description 测试序列ID对序列化的影响,序列化之后修改serialVersionUID,再进行反序列化
* @author shiPengYu
* @date 2022/4/17
*/
public class TransientStudy {
@Test
public void serialization() throws Exception{
// 序列化
File objFile = new File("./src/transientstudy/obj.txt");
if (!objFile.exists()){
objFile.createNewFile();
}
ObjectOutput oo = new ObjectOutputStream(new FileOutputStream(objFile));
People people = new People("张三","男",25);
System.out.println("序列化: "+people.toString());
oo.writeObject(people);
oo.close();
}
@Test
public void deserialize() throws Exception{
// 反序列化
File objFile = new File("./src/transientstudy/obj.txt");
ObjectInput oi = new ObjectInputStream(new FileInputStream(objFile));
People people1 = (People) oi.readObject();
System.out.println("反序列化: "+people1.toString());
// 释放
oi.close();
}
}
class People implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
private String sex;
private transient int age;
public People(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
public People() {
}
@Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
'}';
}
}
测试结果:出现异常,并指明serialVersionUID不一致
serialVersionUID = 1L
serialVersionUID = 2L
3、不包含静态变量
序列化时,属性不包括类属性—静态成员变量。
code 4:
package transientstudy;
import org.junit.Test;
import java.io.*;
/**
* description 测试序列化是否包含静态变量
* @author shiPengYu
* @date 2022/4/17
*/
public class TransientStudy {
@Test
public void serialization() throws Exception{
// 序列化
File objFile = new File("./src/transientstudy/obj.txt");
if (!objFile.exists()){
objFile.createNewFile();
}
ObjectOutput oo = new ObjectOutputStream(new FileOutputStream(objFile));
People people = new People("张三","男",25);
People.nationality = "China";
System.out.println("序列化: "+people.toString());
oo.writeObject(people);
oo.close();
}
@Test
public void deserialize() throws Exception{
// 反序列化
File objFile = new File("./src/transientstudy/obj.txt");
ObjectInput oi = new ObjectInputStream(new FileInputStream(objFile));
People people1 = (People) oi.readObject();
System.out.println("反序列化: "+people1.toString());
// 释放
oi.close();
}
}
class People implements Serializable {
private static final long serialVersionUID = 1L;
public static String nationality;
private String name;
private String sex;
private transient int age;
public People(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
public People() {
}
@Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
'}';
}
}
运行测试结果显示:类成员nationality的值变为初始值。
4、父类必须实现Serializable接口
code 5:
package transientstudy;
import org.junit.Test;
import java.io.*;
/**
* description 测试序列化类的父类是否必须同样实现Serializable接口(答案是:必须),
* 测试时,先以实现的测试序列化与反序列化,然后取消父类的实现,再次测试序列化与反序列化。
* @author shiPengYu
* @date 2022/4/17
*/
public class TransientStudy {
@Test
public void serialization() throws Exception{
// 序列化
File objFile = new File("./src/transientstudy/obj.txt");
if (!objFile.exists()){
objFile.createNewFile();
}
ObjectOutput oo = new ObjectOutputStream(new FileOutputStream(objFile));
Worker worker = new Worker("张三","男",25,"teacher");
oo.writeObject(worker);
System.out.println("序列化:"+worker.toString());
oo.close();
}
@Test
public void deserialize() throws Exception{
// 反序列化
File objFile = new File("./src/transientstudy/obj.txt");
ObjectInput oi = new ObjectInputStream(new FileInputStream(objFile));
Worker worker = (Worker) oi.readObject();
System.out.println("反序列化:"+worker.toString());
// 释放
oi.close();
}
}
class People
//implements Serializable
{
// private static final long serialVersionUID = 1L;
// public static String nationality;
protected String name;
protected String sex;
protected transient int age;
public People(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
public People() {
}
}
class Worker extends People implements Serializable{
private static final long serialVersionUID = 2L;
private String work;
public Worker(String name, String sex, int age,String work) {
super(name, sex, age);
this.work = work;
}
@Override
public String toString() {
return "Worker{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
", work='" + work + '\'' +
'}';
}
}
运行测试结果:父类对象全部变为初始值。
反序列化失败并报错java.io.InvalidClassException: transientstudy.Worker; no valid constructor
5、序列化与反序列化的类路径应一致。
基于code 2的程序,执行完序列化测试后,修改类路径(由transientstudy/TransientStudy.java改为transientstudy/test/TransientStudy.java)后再进行反序列化测试。反序列化失败,报错如下:
Externalizable接口
上述的方式都是通过实现Serializable接口,采用默认的序列化实现。可以通过实现Externalizable接口,重写它的两个抽象方法,来自定义自己的序列化实现。
注意点:通过实现Externalizable接口进行序列化时,需要提供public修饰的无参构造,因为反序列化时,是先调用无参构造来创建对象,再将反序列化的值进行覆盖。不提供则会报错java.io.InvalidClassException: transientstudy.People; no valid constructor
code 6:
package transientstudy;
import javafx.concurrent.Worker;
import org.junit.Test;
import java.io.*;
/**
* description 测试序列化类的父类是否必须同样实现Serializable接口(答案是:必须),
* 测试时,先以实现的测试序列化与反序列化,然后取消父类的实现,再次测试序列化与反序列化。
* @author shiPengYu
* @date 2022/4/17
*/
public class TransientStudy {
@Test
public void serialization() throws Exception{
// 序列化
File objFile = new File("./src/transientstudy/obj.txt");
if (!objFile.exists()){
objFile.createNewFile();
}
ObjectOutput oo = new ObjectOutputStream(new FileOutputStream(objFile));
People people = new People("张三","男",25);
oo.writeObject(people);
System.out.println(people.toString());
oo.close();
}
@Test
public void deserialize() throws Exception{
// 反序列化
File objFile = new File("./src/transientstudy/obj.txt");
ObjectInput oi = new ObjectInputStream(new FileInputStream(objFile));
People people = (People) oi.readObject();
System.out.println(people.toString());
// 释放
oi.close();
}
}
class People
implements Externalizable
{
private static final long serialVersionUID = 1L;
private String name;
private String sex;
private transient int age;
public People(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
public People() {
}
@Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
'}';
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
测试结果
实现为空时,反序列化也为null了。
修改序列化反序列化方法: code 7:
@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();
}
测试结果:
发现:自定义序列化实现时,忽略了sex字段,对应的字段就为初始值。与使用transient关键字同样的效果。
自定义实现需要注意的点
1、序列化与反序列化的属性顺序不能乱。
如图,name的值与sex的值交换了,在类型不符合时还有可发生类型转换异常。
2、序列化与反序列化时的属性数量保持一致
反序列化属性数量 < 序列化属性数量
反序列化属性数量 > 序列化属性数量
一次性序列化、反序列化多个对象
在进行多个对象的序列化与反序列化操作时,只需要依次写入,读取就行了,就像遍历集合一样简单。 code 7:
package transientstudy;
import org.junit.Test;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
/**
* description 测试序列化、反序列化多个对象
*
* @author shiPengYu
* @date 2022/4/17
*/
public class TransientStudy {
@Test
public void serialization() throws Exception {
// 序列化
File objFile = new File("./src/transientstudy/obj.txt");
if (!objFile.exists()) {
objFile.createNewFile();
}
ObjectOutput oo = new ObjectOutputStream(new FileOutputStream(objFile));
People[] peoples = new People[]{new People("张三", "男", 25),
new People("李四", "男", 22),
new People("马冬梅", "女", 20)};
for (People p : peoples) {
oo.writeObject(p);
}
System.out.println(Arrays.toString(peoples));
oo.close();
}
@Test
public void deserialize() throws Exception {
// 反序列化
File objFile = new File("./src/transientstudy/obj.txt");
InputStream in = new FileInputStream(objFile);
ObjectInput oi = new ObjectInputStream(in);
ArrayList<People> arrayListPeople = new ArrayList<>();
People p;
while (in.available() > 0) {
p = (People) oi.readObject();
arrayListPeople.add(p);
}
System.out.println(arrayListPeople);
// 释放
oi.close();
}
}
class People implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String sex;
private int age;
public People(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
public People() {
}
@Override
public String toString() {
return "People{" +
"name='" + name + ''' +
", sex='" + sex + ''' +
", age=" + age +
'}';
}
}
测试结果
画图总结
java序列化整体示意图
java序列化涉及类
参考:
Java对象的序列化与反序列化-HollisChuang's Blog
# Java Serialization(Java 序列化)