实践 Java 序列化与反序列化:实现对象的持久化存储与网络传输,掌握 Serializable 接口与 Externalizable 接口的区别。

好的,各位编程界的才子佳人,大家好!我是你们的老朋友,人称“代码界的段子手”的程序猿老王。今天,咱们要一起扒一扒 Java 序列化与反序列化的底裤,哦不,是原理!

引子:对象也想有个“家”?

话说,咱们辛辛苦苦 new 出来的对象,在 JVM 里活蹦乱跳,好不热闹。可是,一旦程序关了,JVM 嗝屁了,这些对象也就跟着灰飞烟灭了。这就像辛辛苦苦攒了一辈子的钱,结果人没了,钱也跟着进了银行保险柜,想想都心疼!

难道对象就不能有个“家”,可以永久保存,随时取用吗?🤔

当然可以!这就是序列化和反序列化的意义所在。它们就像对象的“搬家公司”,负责把对象从内存里搬到硬盘上(序列化),或者从硬盘上搬回内存里(反序列化)。

第一章:序列化:对象“变形记”

序列化,简单来说,就是把一个 Java 对象转换成一串字节流,以便存储到文件、数据库,或者通过网络传输。这个过程就像把一个活生生的人,变成了一堆乐高积木,虽然模样变了,但本质还在。

1.1 Serializable 接口:对象的“身份证”

要让一个对象能够被序列化,最简单的方法就是让它实现 java.io.Serializable 接口。这个接口就像对象的“身份证”,有了它,JVM 才知道这个对象可以被序列化。

import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 省略 getter 和 setter 方法

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}

注意,Serializable 接口是一个标记接口,它里面没有任何方法。JVM 看到这个接口,就知道该怎么处理了。

1.2 ObjectOutputStream:序列化的“搬运工”

有了“身份证”,还得有“搬运工”才能把对象搬走。这个“搬运工”就是 java.io.ObjectOutputStream 类。

import java.io.*;

public class SerializationDemo {
    public static void main(String[] args) {
        Person person = new Person("老王", 30);

        try (FileOutputStream fileOutputStream = new FileOutputStream("person.ser");
             ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {

            objectOutputStream.writeObject(person);
            System.out.println("对象序列化成功!");

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

这段代码做了什么呢?

  1. 创建了一个 Person 对象。
  2. 创建了一个 FileOutputStream,用于将数据写入到 person.ser 文件中。
  3. 创建了一个 ObjectOutputStream,它负责把 Person 对象转换成字节流,并写入到 FileOutputStream 中。
  4. 调用 writeObject() 方法,把 Person 对象序列化到文件中。

运行这段代码,你会发现生成了一个 person.ser 文件。这个文件里存储的就是 Person 对象的字节流。

1.3 序列化注意事项:有些秘密不能说

并非所有字段都可以被序列化。如果一个字段不想被序列化,可以使用 transient 关键字修饰。transient 修饰的字段,在序列化时会被忽略,反序列化后会恢复为默认值。

import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    private int age;
    private transient String password; // 不希望被序列化

    public Person(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }

    // 省略 getter 和 setter 方法

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + ''' +
                ", age=" + age +
                ", password='" + password + ''' +
                '}';
    }
}

为什么要有 transient 关键字呢?

  • 安全考虑: 有些敏感信息,比如密码、银行卡号,不应该被序列化到磁盘上,以免泄露。
  • 性能考虑: 有些字段的值可以通过其他字段计算出来,不需要被序列化,以节省存储空间和序列化时间。

第二章:反序列化:对象“复活记”

反序列化,就是把一串字节流转换成 Java 对象。这个过程就像把一堆乐高积木,重新拼成一个活生生的人。

2.1 ObjectInputStream:反序列化的“还原师”

反序列化的“还原师”是 java.io.ObjectInputStream 类。

import java.io.*;

public class DeserializationDemo {
    public static void main(String[] args) {
        try (FileInputStream fileInputStream = new FileInputStream("person.ser");
             ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {

            Person person = (Person) objectInputStream.readObject();
            System.out.println("对象反序列化成功!" + person);

        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

这段代码做了什么呢?

  1. 创建了一个 FileInputStream,用于从 person.ser 文件中读取数据。
  2. 创建了一个 ObjectInputStream,它负责把字节流转换成 Person 对象。
  3. 调用 readObject() 方法,从文件中读取字节流,并反序列化成 Person 对象。

运行这段代码,你会看到控制台输出了 Person 对象的信息。

2.2 反序列化注意事项:版本不兼容的“悲剧”

反序列化时,需要确保类的版本号与序列化时的版本号一致。如果类的结构发生了变化,比如增加了字段、删除了字段,或者修改了字段的类型,那么反序列化可能会失败。

为了解决版本兼容性问题,可以显式地指定类的版本号,即 serialVersionUID

import java.io.Serializable;

public class Person implements Serializable {
    private static final long serialVersionUID = 1L; // 显式指定版本号
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 省略 getter 和 setter 方法

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}

显式指定 serialVersionUID 的好处是:即使类的结构发生了微小的变化,只要 serialVersionUID 不变,反序列化仍然可以成功。

第三章:Externalizable 接口:对象的“自主权”

除了 Serializable 接口,Java 还提供了 java.io.Externalizable 接口。Externalizable 接口提供了更强大的控制权,允许开发者完全自定义序列化和反序列化的过程。

3.1 Externalizable 接口:对象的“私人订制”

Externalizable 接口继承自 Serializable 接口,但它强制开发者实现 writeExternal()readExternal() 方法。

import java.io.*;

public class Student implements Externalizable {
    private String name;
    private int age;
    private String className;

    public Student() {
        // 必须提供一个无参构造函数
    }

    public Student(String name, int age, String className) {
        this.name = name;
        this.age = age;
        this.className = className;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // 自定义序列化逻辑
        out.writeObject(name);
        out.writeInt(age);
        out.writeObject(className);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // 自定义反序列化逻辑
        name = (String) in.readObject();
        age = in.readInt();
        className = (String) in.readObject();
    }

    // 省略 getter 和 setter 方法

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + ''' +
                ", age=" + age +
                ", className='" + className + ''' +
                '}';
    }
}

注意,实现 Externalizable 接口的类,必须提供一个无参构造函数。这是因为在反序列化时,JVM 会先调用无参构造函数创建一个对象,然后再调用 readExternal() 方法来初始化对象的字段。

3.2 Serializable vs. Externalizable:谁更胜一筹?

特性 Serializable Externalizable
实现方式 标记接口,无需实现任何方法 继承自 Serializable 接口,需要实现 writeExternal()readExternal() 方法
序列化方式 默认序列化,JVM 自动处理 自定义序列化,开发者完全控制
性能 相对较低,因为 JVM 需要通过反射来获取对象的字段值 相对较高,因为开发者可以直接写入和读取字段值
安全性 相对较低,因为所有非 transient 字段都会被序列化 相对较高,因为开发者可以控制哪些字段被序列化
版本兼容性 依赖 serialVersionUID,如果类的结构发生了变化,可能会导致反序列化失败 需要开发者自己处理版本兼容性问题
适用场景 简单对象,对性能要求不高,对安全性要求不高 复杂对象,对性能要求高,对安全性要求高,需要自定义序列化逻辑
无参构造函数 不需要 需要

总的来说,Serializable 接口简单易用,适用于大多数场景。Externalizable 接口提供了更强大的控制权,适用于对性能和安全性有较高要求的场景。

第四章:序列化与反序列化的应用场景

序列化和反序列化在实际开发中有着广泛的应用。

4.1 持久化存储:对象的“时光机”

最常见的应用场景就是把对象保存到文件或数据库中,实现对象的持久化存储。这样,即使程序关闭了,对象的数据也不会丢失。

4.2 网络传输:对象的“快递员”

在分布式系统中,需要把对象通过网络传输到不同的节点。序列化和反序列化可以将对象转换成字节流,方便在网络上传输。

4.3 缓存:对象的“备忘录”

在缓存系统中,可以把对象序列化后存储到缓存中,以提高访问速度。

4.4 RMI (Remote Method Invocation):对象的“远程调用”

RMI 允许一个 JVM 调用另一个 JVM 中的对象的方法。序列化和反序列化是 RMI 的基础。

总结:对象的“变形金刚”

序列化和反序列化就像对象的“变形金刚”,可以把对象转换成字节流,方便存储、传输和缓存。掌握了序列化和反序列化的原理,你就可以更好地理解 Java 对象的生命周期,编写更高效、更安全的代码。

好了,今天的分享就到这里。希望大家有所收获!如果觉得老王讲得还不错,记得点个赞哦!👍

彩蛋:序列化与单例模式

如果一个类是单例模式,那么在反序列化时,可能会创建多个实例,破坏单例的特性。为了解决这个问题,可以实现 readResolve() 方法。

import java.io.Serializable;

public class Singleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final Singleton instance = new Singleton();

    private Singleton() {
        // 私有构造函数,防止外部创建实例
    }

    public static Singleton getInstance() {
        return instance;
    }

    private Object readResolve() {
        // 在反序列化时,返回已有的实例
        return instance;
    }
}

readResolve() 方法会在反序列化完成后被调用。在这个方法里,我们可以返回已有的单例实例,从而保证单例的特性。

希望这个彩蛋对大家有所帮助!祝大家编程愉快!😊

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注