一文带你了解序列化与反序列化基本原理与操作

在这里插入图片描述


在这里插入图片描述

一、什么是序列化与反序列化

序列化是指将对象转换为字节序列的过程,以便于存储或传输。在序列化过程中,对象的状态信息将被转换为字节流,可以保存到文件中或通过网络传输给其他计算机。反序列化则是将字节序列恢复为对象的过程。

通过反序列化,可以实现以下功能:

对象恢复:通过反序列化操作,可以从存储的字节流中还原对象的状态。这使得可以在程序重启后,读取保存的字节流并重新构造对象,从而快速恢复对象的状态。

分布式应用:在分布式系统中,可以将对象进行序列化,并在不同的计算机之间进行传输。接收方可以通过反序列化操作将字节序列转换为可操作的对象。

远程调用:某些远程通信框架使用序列化和反序列化来实现远程方法调用。方法调用和参数会被序列化成字节流发送给远程服务,然后通过反序列化在远程服务端还原方法调用和参数。


反序列化是指将字节序列恢复为对象的过程。在序列化中,对象的状态信息被转换为字节流以便于存储或传输,而在反序列化中,这些字节流将被重新转换为原始对象的状态。

需要注意以下几点:

在进行反序列化时,要确保字节序列的来源是可信的,以避免安全风险。

反序列化过程可能引发版本兼容性问题。如果在序列化对象之后,修改了类的结构,尤其是删除或更改字段、方法等,那么在反序列化时可能会导致无法正确恢复对象或发生异常。

在反序列化时,Java虚拟机会使用对象中的默认构造函数来创建对象实例,然后通过反射

二、为什么我们需要序列化与反序列化

一开始学的时候还是不知道的。

序列化与反序列化的设计就是用来传输数据的。

当两个进程进行通信的时候,可以通过序列化反序列化来进行传输。

持久化:对象是存储在JVM中的堆区的,但是如果JVM停止运行了,对象也不存在了。序列化可以将对象转化成字节序列,可以写进硬盘文件中实现持久化。在新开启的JVM中可以读取字节序列进行反序列化成对象。

网络传输:网络直接传输数据,但是无法直接传输对象,可在传输前序列化,传输完成后反序列化成对象。所以所有可在网络上传输的对象都必须是可序列化的。

三、步骤说明

序列化和反序列化是将对象转换为字节流或从字节流还原对象的过程。实现这两个过程的一般步骤如下:

序列化的实现步骤

1. 确保类可以序列化:需要检查要序列化的类是否实现了 Serializable 接口。如果没有实现该接口,需要添加该接口的实现。

2. 创建输出流:使用一个输出流(例如 FileOutputStream)来写入字节流。

3. 创建对象输出流:使用 ObjectOutputStream 类创建一个对象输出流,并将其与步骤 2 中的输出流关联起来。

4. 序列化对象:使用对象输出流的 writeObject() 方法,将需要序列化的对象写入字节流中。

5. 关闭流:关闭对象输出流和输出流。


反序列化的实现步骤:

1. 确保类可以反序列化:需要检查要反序列化的类是否实现了 Serializable 接口。如果没有实现该接口,需要添加该接口的实现。

2. 创建输入流:使用一个输入流(例如 FileInputStream)来读取字节流。

3. 创建对象输入流:使用 ObjectInputStream 类创建一个对象输入流,并将其与步骤 2 中的输入流关联起来。

4. 反序列化对象:使用对象输入流的 readObject() 方法,从字节流中读取对象并进行反序列化。

5. 关闭流:关闭对象输入流和输入流。

四、注意说明

序列化和反序列化过程中,要保证被序列化和反序列化的类具有相同的 serialVersionUID 值,以确保版本兼容性。

另外,在处理序列化和反序列化时还需要注意以下几点:

  • 静态字段和 transient 标记的字段不会进行序列化和反序列化。
  • 序列化可能引发版本兼容性问题,例如序列化后修改了类的结构,在反序列化时可能导致无法正确恢复对象。
  • 对象的序列化和反序列化可能导致对应的类的构造函数不被调用。

五、代码说明

序列化

实现可序列化的类需要实现 java.io.Serializable 接口。该接口是一个标记接口,没有任何方法定义。通过实现 Serializable 接口,可以使得对象的所有非静态字段都可以被序列化。

import java.io.*;

public class SerializationExample {
    
    
    public static void main(String[] args) {
    
    
        // 序列化对象
        try (OutputStream fileOut = new FileOutputStream("object.ser");
             ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
    
    

            Person person = new Person("Alice", 25);
            out.writeObject(person);

            System.out.println("Object serialized successfully.");

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

        // 反序列化对象
        try (InputStream fileIn = new FileInputStream("object.ser");
             ObjectInputStream in = new ObjectInputStream(fileIn)) {
    
    

            Person restoredPerson = (Person) in.readObject();
            System.out.println("Deserialized object: " + restoredPerson);

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

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

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

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

上述示例中,Person 类实现了 Serializable 接口,使得该类的实例可以被序列化。通过创建 ObjectOutputStream 对象并调用 writeObject() 方法,将 Person 对象序列化为字节流并保存到文件中。然后通过创建 ObjectInputStream 对象并调用 readObject() 方法,可以从文件中读取字节流并进行反序列化,还原为 Person 对象。


反序列化
反序列化可以通过 java.io.ObjectInputStream 类来完成。该类提供了 readObject() 方法用于从输入流中读取字节序列并进行反序列化。

import java.io.*;

public class DeserializationExample {
    
    
    public static void main(String[] args) {
    
    
        // 反序列化对象
        try (InputStream fileIn = new FileInputStream("object.ser");
             ObjectInputStream in = new ObjectInputStream(fileIn)) {
    
    

            Person restoredPerson = (Person) in.readObject();
            System.out.println("Deserialized object: " + restoredPerson);

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

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

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

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

我们通过创建 ObjectInputStream 对象并调用其 readObject() 方法,从名为 “object.ser” 的文件中读取字节流,并将其反序列化为 Person 对象。最后再打印恢复的对象。

六、序列化与反序列化原理

序列化
1.对象图:要进行序列化,需要将整个对象以及对象所引用的其他对象一起组成一个对象图。对象图表示了对象之间的关系和结构。
对象图表示了在进行序列化时涉及的对象以及对象之间的关系和结构。对象图是由被序列化的对象及其引用的其他对象组成的。
例如,考虑以下类的对象图示例:

class Person implements Serializable {
    
    
    String name;
    int age;
    Address address;
}

class Address implements Serializable {
    
    
    String city;
    String street;
}

Person person = new Person();
person.name = "Alice";
person.age = 25;

Address address = new Address();
address.city = "New York";
address.street = "123 Main St";

person.address = address;

Person 类和 Address 类都实现了 Serializable 接口,因此可以被序列化。
对象图示例如下:

      +------------+
      |   Person   |
      +------------+
      |   name     |
      |   age      |
      |   address--+--+
      +------------+  |
                      |
                      V
                 +------------+
                 |  Address   |
                 +------------+
                 |   city     |
                 |   street   |
                 +------------+

在对象图中,有一个 Person 对象和一个关联的 Address 对象。Person 对象包含了一个 name 字段、一个 age 字段和一个 address 字段,其中 address 字段引用了一个 Address 对象。而 Address 对象则包含了一个 city 字段和一个 street 字段。

在进行序列化时,整个对象图会被递归遍历,并将每个对象的状态写入字节流中。通过对象图,可以在反序列化时正确恢复对象之间的引用关系和结构。


2.写入字节流:序列化过程会通过将对象的状态(即对象的字段值)写入字节流中来实现。被序列化的对象的各个字段将按照一定的顺序被写入字节流中,形成一种可传输或存储的形式。

这个过程涉及以下几个步骤:

  • 创建输出流:首先需要创建一个输出流,可以是文件输出流、网络输出流或其他类型的输出流,用于存储序列化后的字节流。
  • 打开对象流:在 Java 中,通常使用 ObjectOutputStream
    类作为对象流,用于将对象写入输出流中。通过将输出流传递给对象流的构造方法,可以打开对象流,准备进行对象的序列化操作。
  • 写入对象:使用对象流提供的方法,如 writeObject(Object
    obj),将要序列化的对象写入对象流。对象流会自动处理对象的序列化操作,包括将对象的字段值以特定的格式写入字节流中。
  • 关闭流:在完成对象的写入操作后,需要关闭对象流和输出流,释放资源并确保数据正确写入。

具体的代码示例如下:

try {
    
    
    // 1. 创建输出流
    FileOutputStream fileOut = new FileOutputStream("data.ser");
    
    // 2. 打开对象流
    ObjectOutputStream objOut = new ObjectOutputStream(fileOut);
    
    // 3. 写入对象
    objOut.writeObject(person); // person 是要序列化的对象
    
    // 4. 关闭流
    objOut.close();
    fileOut.close();
    
    System.out.println("对象已成功序列化并写入字节流。");
} catch (IOException e) {
    
    
    e.printStackTrace();
}

首先创建了一个 FileOutputStream 对象来代表要写入的文件输出流。然后使用这个输出流创建一个 ObjectOutputStream 对象,打开对象流。之后调用 writeObject() 方法将要序列化的对象 person 写入对象流中。最后关闭对象流和输出流。


3.对象标识:序列化过程中,为了确保重复引用同一个对象时能够恢复为同一个对象,使用对象标识来对每个对象进行唯一标识。在序列化和反序列化过程中,将对象与对象标识相关联,以正确还原对象之间的引用关系。

在进行对象的序列化和反序列化过程中,为了能够正确地恢复对象的引用关系和结构,每个被序列化的对象都需要一个唯一的标识。

在 Java 中,对象的标识主要依赖于两个机制:序列化编号(Serialization ID)和对象引用(Object References)

  • 序列化编号: 每个类都有一个默认的序列化编号。这个编号会根据类的结构自动生成,可以通过 serialVersionUID
    字段指定自定义的序列化编号。当对一个类进行序列化时,会将该编号写入到序列化数据中。在反序列化时,会比较读取到的序列化编号与当前类的序列化编号,以确定是否兼容进行反序列化操作。
    -对象引用: 当对象图中存在相同的对象或相同引用的对象时,为了保持对象的引用关系,引用对象只会被序列化一次,并在后续重建对象时使用相同的引用。这样可以避免重复序列化相同的对象,减小序列化数据的大小。

通过以上两个机制,序列化器能够正确地处理对象的标识问题,并在反序列化时恢复对象的引用关系。

需要注意的是,如果类的结构发生了变化,例如添加、删除或修改了字段或方法,那么原来的序列化编号可能不能与新的类匹配,可能导致无法正确反序列化。在这种情况下,应该显式地指定一个自定义的序列化编号并进行更新,以确保序列化和反序列化的兼容性。


4.序列化格式:不同的编程语言和平台可能采用不同的序列化格式,在 Java 中常用的是二进制格式。序列化格式定义了如何将对象的状态信息写入字节流以及如何从字节流中读取并恢复对象的状态。


5关键方法:在 Java 中,通过实现 Serializable 接口,对象的序列化和反序列化由 JVM 自动处理。在序列化过程中,会调用对象的 writeObject() 方法进行字段的写入操作;在反序列化过程中,会调用对象的 readObject() 方法进行字段的读取和恢复操作。这些方法可以根据需要进行自定义实现。


反序列化
反序列化是将序列化的数据重新转换为对象的过程。它的实现原理主要涉及以下几个步骤:

读取数据: 从序列化的数据中读取字节流。

校验版本: 校验序列化数据中的版本信息,确保与当前程序的序列化版本兼容。

创建对象: 根据读取到的数据类型和结构,在内存中创建一个空对象。

读取字段: 按序读取序列化数据中保存的字段值,并将这些值设置到对象对应的字段中。这些值可以是基本类型、引用类型或其他对象。

解析引用: 如果序列化数据中存在对象引用关系,解析引用的过程会确保相同的引用在对象图中只被加载一次,以保持正确的对象引用关系。

执行构造函数: 执行对象的构造函数,完成对象初始化的操作。

返回对象: 返回经过反序列化后的完整对象。

猜你喜欢

转载自blog.csdn.net/m0_68987535/article/details/131566304