序列化和反序列化

一、序列化:将 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 对象。以下是一些使用序列化的示例:
①以面向对象的方式将数据存储到磁盘上的文件。例如,Redis存储Student对象的列表。
②将程序的状态保存在磁盘上。例如,保存游戏状态。
③通过网络以表单对象形式发送数据。例如,在聊天应用程序中以对象形式发送消息。

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

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

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

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

四、Java中的序列化如何工作

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

序列化算法一般会按步骤做如下事情:
①将对象实例相关的类元数据输出。
②递归地输出类的超类描述直到不再有超类。
③类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
④从上至下递归输出实例的数据

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

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

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

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

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

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也将更改。想象一下这种情况:

①一个程序,将Student类的某些对象存储到文件中。Student类没有显式声明的serialVersionUID。
②而后更新了Student类(比如新增了一个私有方法),现在自动生成的serialVersionUID也被更改了。
③该程序无法反序列化先前编写的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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353