Java 序列化与反序列化

1,190 阅读9分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

什么是序列化与反序列化

序列化:是一种将对象转换为可传输的或者可存储的数据格式的过程,序列化后可用于网络进行数据传输,也可用于数据持久化。但是一般常用于网络传输。

反序列化:就是序列化的逆操作。 序列化与反序列化之间的数据格式一般是二进制的字节码,有网友说还有xml格式,但是没见过,也没用过,就暂不做讨论。

Java中的序列化与反序列化

Java是一门面向对象的编程语言,所以Java中的序列化与反序列化也就可以看成是对对象的持久化,以及网络传输,因为对象里面的信息就是数据。

有哪些应用?🤔

1、对象存在Java虚拟机的堆内存中,Java虚拟机又存在于电脑的内存条里,一旦当前运行的Java虚拟机进程被干掉,那么所谓的对象也就不存在了。所以运用Java的序列化机制,我们就可以在对象生命周期结束之前,也就是被jvm回收之前将对象持久化到操作系统的文件系统中,也可以持久化到数据库(但是究其根本都是存到磁盘,具体的视需求、场景而定),这样在下一次启动Java程序启动时,或者某一对象被销毁后,仍然可以从文件中将对象通过反序列化操作还原对象(当然前提是得事先持久化存储)。

2、用于对象的深度克隆(clone)操作。

3、网络传输,也就是跨jvm之间通过网络传递Object序列化后者的二进制字节流来达到传递对象数据的作用。

前提:实现Serializable接口

想要使得对象能支持序列化与反序列化,就必须实现java.io.Serializable接口,这是一个标识性接口,是给编译器看的,里面没有任何内容。

image.png

但是对没有实现这个接口的类对象进行实例化,会抛出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 +
                '}';
    }
}

测试结果

image.png 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不一致

image_2.png serialVersionUID = 1L

image_3.png 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的值变为初始值。

image.png

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 + '\'' +
                '}';
    }
}

运行测试结果:父类对象全部变为初始值。 image_5.png

反序列化失败并报错java.io.InvalidClassException: transientstudy.Worker; no valid constructor

5、序列化与反序列化的类路径应一致。

基于code 2的程序,执行完序列化测试后,修改类路径(由transientstudy/TransientStudy.java改为transientstudy/test/TransientStudy.java)后再进行反序列化测试。反序列化失败,报错如下:

image_6.png

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 {
    }
}

测试结果

image_7.png 实现为空时,反序列化也为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();

    }

测试结果:

image_8.png 发现:自定义序列化实现时,忽略了sex字段,对应的字段就为初始值。与使用transient关键字同样的效果。

自定义实现需要注意的点

1、序列化与反序列化的属性顺序不能乱。

如图,name的值与sex的值交换了,在类型不符合时还有可发生类型转换异常。

image_9.png

2、序列化与反序列化时的属性数量保持一致

image_10.png 反序列化属性数量 < 序列化属性数量

image_11.png 反序列化属性数量 > 序列化属性数量

一次性序列化、反序列化多个对象

在进行多个对象的序列化与反序列化操作时,只需要依次写入,读取就行了,就像遍历集合一样简单。 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 +
                '}';
    }
}

测试结果

image.png

画图总结

Java序列化.drawio.png java序列化整体示意图 image.png java序列化涉及类

参考:
Java对象的序列化与反序列化-HollisChuang's Blog
# Java Serialization(Java 序列化)