序列化和反序列化

一、序列化:将 Java 对象转换成字节流的过程

1️⃣序列化过程:是指把一个 Java 对象变成二进制内容,实质上就是一个 byte[]。因为序列化后可以把 byte[] 保存到文件中,或者把 byte[] 通过网络传输到远程(IO),如此就相当于把 Java 对象存储到文件或者通过网络传输出去了。
2️⃣一个 Java 对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下:

public interface Serializable {}

Serializable 没有定义任何方法,它是一个空接口。这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。

二、反序列化:将字节流转换成 Java 对象的过程。

反序列化过程:把一个二进制内容(也就是 byte[])变回 Java 对象。有了反序列化,保存到文件中的 byte[] 又可以“变回” Java 对象,或者从网络上读取 byte[] 并把它“变回” Java 对象。以下是一些使用序列化的示例:

  1. 以面向对象的方式将数据存储到磁盘上的文件。例如,Redis 存储 Student 对象的列表。
  2. 将程序的状态保存在磁盘上。例如保存游戏状态。
  3. 通过网络以表单对象形式发送数据。例如,在聊天应用程序中以对象形式发送消息。

三、为什么需要序列化与反序列化

当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。当两个 Java 进程进行通信时,需要 Java 序列化与反序列化实现进程间的对象传送。换句话说,一方面,发送方需要把这个 Java 对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出 Java 对象。

优点:

  1. 实现了数据的持久化,序列化可以把数据永久地保存到硬盘上(通常存放在文件里)。
  2. 通过序列化以字节流的形式使对象在网络中进行传递和接收。
  3. 通过序列化在进程间传递对象。

注意事项:

  1. 某个类可以被序列化,则其子类也可以被序列化。
  2. 声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient 表示临时数据。
  3. 反序列化读取序列化对象的顺序要保持一致。

四、Java 序列化如何工作

Java 对象在网络上传输或者持久化存储到文件中时,需要进行序列化处理。当且仅当对象的类实现java.io.Serializable时,该对象才有资格进行序列化,然而真正的序列化动作不需要靠它完成。序列化是一个标记接口(不包含任何方法),该接口告诉Java虚拟机(JVM)该类的对象已准备好写入持久性存储或通过网络进行读取。

序列化算法一般会按步骤做如下事情:

  1. 将对象实例相关的类元数据输出。
  2. 递归地输出类的超类描述直到不再有超类。
  3. 类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
  4. 从上至下递归输出实例的数据。

默认情况下,JVM 负责编写和读取可序列化对象的过程。序列化/反序列化功能通过对象流类的以下两种方法公开:
1️⃣ObjectOutputStream.writeObject(Object):将可序列化的对象写入输出流。如果要序列化的某些对象未实现java.io.Serializable,则此方法将引发NotSerializableException。

按照提示,由源码一直跟到ObjectOutputStream的writeObject0()底层:

2️⃣ObjectInputStream.readObject():从输入流读取,构造并返回一个对象。如果找不到序列化对象的类,则此方法将引发ClassNotFoundException。readObject() 返回一个 Object 类型的对象,因此需要将其强制转换为可序列化的类,在这种情况下为String类。

如果序列化使用的类有问题,则这两种方法都将引发InvalidClassException,如果发生 I/O 错误,则将引发IOException。NotSerializableException和InvalidClassException是IOException的子类。

举个例子,对 Student 类对象序列化到一个名为 student.txt 的文本文件中,然后再通过文本文件反序列化成 Student 类对象:
①Student 类:

@Data
public class Student implements Serializable {
    private String name;
    private Integer age;
    private Integer score;
    @Override
    public String toString() {
        return "Student:" + '\n' +
        "name = " + this.name + '\n' +
        "age = " + this.age + '\n' +
        "score = " + this.score + '\n'
        ;
    }
    // ... 其他省略 ...
}

②序列化

public static void serialize() throws IOException {
    Student student = new Student();
    student.setName("new");
    student.setAge(18);
    student.setScore(100);
    ObjectOutputStream objectOutputStream = 
        new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
    objectOutputStream.writeObject( student );
    objectOutputStream.close();
    System.out.println("序列化成功!已经生成student.txt文件");
    System.out.println("============================");
}

③反序列化

public static void deserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream objectInputStream = 
        new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
    Student student = (Student) objectInputStream.readObject();
    objectInputStream.close();
    System.out.println("反序列化结果为:");
    System.out.println( student );
}

④运行结果:

序列化成功!已经生成student.txt文件
============================
反序列化结果为:
Student:
name = new
age = 18
score = 100

五、什么是serialVersionUID常数

import java.io.*;
import java.util.*;
 
public class Student extends Person implements Serializable {
    public static final long serialVersionUID = 1234L;
 
    private long studentId;
    private String name;
    private transient int age;
 
    public Student(long studentId, String name, int age) {
        super();
        this.studentId = studentId;
        this.name = name;
        this.age = age;
        System.out.println("Constructor");
    }
 
    public String toString() {
        return String.format("%d - %s - %d", studentId, name, age);
    }
}

如上代码:

  • long serialVersionUID类型的常量。
  • 成员变量age被标记为transient。

serialVersionUID 是一个常数,用于唯一标识可序列化类的版本。从输入流构造对象时,JVM 在反序列化过程中检查此常数。如果正在读取的对象的 serialVersionUID 与类中指定的序列号不同,则 JVM 抛出InvalidClassException。这是为了确保正在构造的对象与具有相同 serialVersionUID 的类兼容。

serialVersionUID 是可选的。如果不显式声明,Java 编译器将自动生成一个。为何要必须显式声明 serialVersionUID?

原因是:自动生成的 serialVersionUID 是基于类的元素(成员变量、方法和构造函数等)计算的。如果这些元素之一发生更改,serialVersionUID 也将更改。想象一下这种情况:

  1. 一个程序,将 Student 类的某些对象存储到文件中。Student 类没有显式声明的 serialVersionUID。
  2. 而后更新了 Student 类(比如新增了一个私有方法),现在自动生成的 serialVersionUID 也被更改了。
  3. 该程序无法反序列化先前编写的 Student 对象,因为那里的 serialVersionUID 不同。JVM 抛出InvalidClassException。

这就是为什么建议为可序列化类显式添加 serialVersionUID 的原因。

六、什么是瞬时变量?

上面 Student 类的成员变量 age 被标记为 transient,JVM 在序列化过程中会跳过瞬态变量。这意味着在序列化对象时不会存储 age 变量的值。因此,如果成员变量不需要序列化,则可以将其标记为瞬态。以下代码将 Student 对象序列化为名为“ students.ser”的文件:

String filePath = "students.ser";
Student student = new Student(123, "John", 22);
try (
    FileOutputStream fos = new FileOutputStream(filePath);
    ObjectOutputStream outputStream = new ObjectOutputStream(fos);
) {
    outputStream.writeObject(student);
} catch (IOException ex) {
    System.err.println(ex);
}

请注意,在序列化对象之前,变量 age 的值为 22。下面的代码从文件中反序列化 Student 对象:

String filePath = "students.ser";
try (
    FileInputStream fis = new FileInputStream(filePath);
    ObjectInputStream inputStream = new ObjectInputStream(fis);
) {
    Student student = (Student) inputStream.readObject();
    System.out.println(student);
} catch (ClassNotFoundException ex) {
    System.err.println("Class not found: " + ex);
} catch (IOException ex) {
    System.err.println("IO error: " + ex);
}

此代码输出如下:
1个
123 - John - 0

七、总结

1️⃣序列化一个对象时,它所引用的所有其他对象也会被序列化,依此类推,直到序列化完整的对象树为止。
2️⃣如果超类实现 Serializable,则其子类会自动执行。
3️⃣反序列化可序列化类的实例时,构造函数将不会运行。
4️⃣如果超类未实现 Serializable,则在反序列化子类对象时,超类构造函数将运行。
5️⃣静态变量未序列化,因为它们不是对象本身的一部分。
6️⃣如果序列化集合或数组,则每个元素都必须可序列化。单个不可序列化的元素将导致序列化失败(NotSerializableException)。
7️⃣JDK 中的可序列化类包括原始包装器(Integer,Long,Double等)、String、Date、collection 类等。对于其他类,请查阅相关的 Javadoc 来了解它们是否可序列化。
8️⃣相关接口及类:
①java.io.Serializable
②java.io.Externalizable
③ObjectOutput
④ObjectInput
⑤ObjectOutputStream
⑥ObjectInputStream

  • 43
    点赞
  • 133
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JFS_Study

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值