序列化与反序列化

一、根本矛盾:内存世界 vs 外部世界

要理解序列化,首先要理解计算机系统里一个根本性的对立:

维度 内存世界(进程内) 外部世界(磁盘/网络)
存储介质 RAM(随机访问) 磁盘、网络传输
数据载体 对象(Object) 字节流(Byte Stream)
生命周期 进程存在则存在,进程消亡则消亡 持久存在,可跨进程、跨机器
访问方式 通过引用直接访问 通过 I/O 读写访问
语义 有类、有方法、有封装 只有 0 和 1,无任何语义

核心矛盾就一句话

内存里精心构造的对象,放到外部世界就只是一堆无意义的字节

序列化就是用来解决这个矛盾的翻译官

  内存世界              序列化/反序列化           外部世界
  ┌──────────┐         ┌─────────┐            ┌──────────┐
  │ Player   │ ─ser──→ │ 字节流   │ ─write──→  │ File/Net │
  │ 对象     │ ←deser─ │ 字节流   │ ←read───   │          │
  └──────────┘         └─────────┘            └──────────┘

二、词源与本义

2.1 词源

Serialize 来自拉丁语 serere(连接、串联),字面意思是"把东西串成一系列"。

在计算机领域,引申为:把对象的状态,按某种顺序"串"成一串字节

De- 是表示"反向、去除"的前缀,Deserialize 就是反过来:从一串字节重新组装出对象。

2.2 一句话定义

序列化(Serialization)= 将内存中对象的完整状态信息,转换成可传输/可存储的字节流格式。

反序列化(Deserialization)= 从字节流中还原出原来的对象。

核心要求只有一个:保真


三、三大刚需场景

为什么非要做"翻译"?因为有三大场景逼着必须把对象变成字节再变回来。

3.1 持久化 —— 让数据"活下去"

痛点:进程一死,内存全丢。写了半天的游戏存档、用户配置、草稿文本,全没了。

序列化的作用:把内存对象变成字节,写入磁盘,进程死了数据还在。

内存中的 User 对象          序列化写入              磁盘文件
┌─────────────────┐                              ┌──────────┐
│ name = "张三"    │ ─── 序列化 ───→  write ───→  │ {        │
│ age  = 28       │                              │  "name":"张三", │
│ email = "..."   │                              │  "age":28 │
│                 │                              │ }        │
└─────────────────┘                              └──────────┘

重启后:
读取文件 → 反序列化 → 重新得到 User 对象

典型应用
- 游戏存档(Player 对象 → 存档文件)
- 配置文件(Properties 对象 → .properties 文件)
- 数据库存储(ORM 把对象序列化成行,存进数据库)
- 文档格式(Word、PDF 本质上都是序列化后的字节流)

3.2 分布式通信 —— 让数据"走出去"

痛点:两个进程在不同机器上,它们的内存物理隔离,不能直接共享对象引用。

进程 A 的 User 对象,进程 B 看不到也摸不到。进程 A 想告诉进程 B,只能把对象"翻译成字节",通过网络发过去。

机器 A 的进程 X                     机器 B 的进程 Y
┌──────────────┐                   ┌──────────────┐
│  User 对象    │    网络传输        │  User 对象    │
│              │ ───字节流──────→   │              │
└──────────────┘                   └──────────────┘
  (序列化)                          (反序列化)

关键洞察

进程 A 的 User 对象和进程 B 的 User 对象,是两个独立的实例

网络上传过去的不是"对象本身",而是"对象的信息"——B 收到信息后,在自己的内存里重新构造一个新的 User 对象

这就是为什么叫"序列化/反序列化"——它是复制,不是共享

典型应用
- RPC 调用(gRPC、Dubbo:把请求对象序列化成字节,网络传输后反序列化)
- REST API(JSON 本质就是序列化格式)
- 消息队列(Kafka/RabbitMQ:生产者序列化消息,消费者反序列化)
- 集群状态同步(Redis 的 RDB/AOF 就是序列化过程)

3.3 跨语言/跨平台 —— 让数据"说同一种话"

痛点:Java 的 User 对象,Python、Go、C++ 读不懂。每个语言的对象模型不一样。

序列化的作用:用一种中立的格式(如 JSON、Protobuf)做中间翻译,任何语言都能序列化和反序列化。

Java User 对象  ──序列化为JSON──→  JSON 字节流  ──反序列化──→  Python User 对象
  (JVM 内存)                                                      (Python 内存)

3.4 三大场景汇总

场景 解决的问题 典型应用
持久化 让对象超越进程生命周期 存档、配置、数据库
分布式通信 让对象跨越进程/机器边界 RPC、MQ、HTTP
跨语言交流 让对象跨越语言边界 JSON、Protobuf

3.5 三大场景的实例

下面用三个可运行的实例,展示序列化在真实场景里的实际用法。

实例一:持久化 —— 游戏存档

进程死了数据还在。用 Jackson 把 Player 对象存成 JSON 文件,下次启动读回来。

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;

// 游戏角色
public class Player {
    public String name;        // 角色名
    public int level;          // 等级
    public int hp;             // 血量
    public String[] inventory; // 背包

    public Player() {}  // Jackson 反序列化需要无参构造
}

public class GameSave {
    private static final ObjectMapper mapper = new ObjectMapper();
    private static final File SAVE_FILE = new File("save.json");

    // 存档:Player 对象 → JSON 文件
    public static void save(Player player) throws Exception {
        mapper.writeValue(SAVE_FILE, player);
        // save.json 内容:{"name":"勇者","level":25,"hp":800,"inventory":["铁剑","药水"]}
        System.out.println("存档成功,进程就算死了,数据还在磁盘上");
    }

    // 读档:JSON 文件 → Player 对象
    public static Player load() throws Exception {
        Player player = mapper.readValue(SAVE_FILE, Player.class);
        System.out.println("读档成功:" + player.name + " Lv." + player.level);
        return player;
    }

    public static void main(String[] args) throws Exception {
        // 第一次运行:创建角色并存档
        Player hero = new Player();
        hero.name = "勇者";
        hero.level = 25;
        hero.hp = 800;
        hero.inventory = new String[]{"铁剑", "药水"};
        save(hero);

        // "进程死了" —— 重新启动后读档
        // 实际开发中这是另一次程序启动
        Player restored = load();
        // restored 和 hero 是两个不同的对象,但状态完全一致
        System.out.println(hero == restored);           // false —— 不是同一个对象
        System.out.println(hero.name.equals(restored.name));  // true  —— 但状态一样
    }
}

herorestored两个独立的对象== 为 false),但状态完全一致。序列化的本质是复制,不是共享。

实例二:分布式通信 —— Socket 传对象

两个进程之间传一个 User 对象。发送方序列化成 JSON 字节流,接收方反序列化回对象。

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.*;
import java.net.*;

// 传输的数据对象
public class User {
    public String name;
    public int age;
    public User() {}
}

// 服务端:接收 User 对象
public class Server {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        ServerSocket server = new ServerSocket(9999);
        System.out.println("服务端启动,等待接收对象...");

        Socket socket = server.accept();
        // 从网络流里读字节,反序列化成 User 对象
        User user = mapper.readValue(socket.getInputStream(), User.class);
        System.out.println("收到对象:" + user.name + "," + user.age + "岁");
        // 输出:收到对象:张三,28岁

        socket.close();
        server.close();
    }
}

// 客户端:发送 User 对象
public class Client {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        Socket socket = new Socket("127.0.0.1", 9999);

        User user = new User();
        user.name = "张三";
        user.age = 28;

        // 把 User 对象序列化成 JSON 字节流,写入网络
        mapper.writeValue(socket.getOutputStream(), user);
        // 网络上跑的不是"对象",是这串字节:{"name":"张三","age":28}

        System.out.println("对象已发送");
        socket.close();
    }
}

客户端的 user 和服务端反序列化出来的 user,在两台机器的两个 JVM 里,是完全独立的两个对象。它们之间只通过一串 JSON 字节产生了联系。

实例三:跨语言 —— Java 写,Python 读

同一个 JSON 字节流,Java 写出来,Python 能读,Go 也能读。这就是"中立格式"的威力。

Java 端(序列化):

import com.fasterxml.jackson.databind.ObjectMapper;

public class CrossLangDemo {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();

        User user = new User();
        user.name = "张三";
        user.age = 28;

        // 序列化成 JSON 字符串
        String json = mapper.writeValueAsString(user);
        System.out.println(json);
        // 输出:{"name":"张三","age":28}

        // 这个字符串可以发给任何语言
    }
}

Python 端(反序列化):

import json

# 收到 Java 发来的 JSON 字符串
json_str = '{"name":"张三","age":28}'

# 反序列化成 Python 字典
user = json.loads(json_str)
print(user['name'])  # 张三
print(user['age'])   # 28

# Python 不需要知道 Java 的 User 类长什么样
# JSON 是中立格式,谁都能读

Java 的 User 对象和 Python 的 dict 字典,结构完全不同。但通过 JSON 这个"中立格式",两个语言的世界被打通了。这就是序列化跨语言的本质。

三个实例的共同模式

不管哪个场景,核心都是同一个闭环:

对象 → 序列化 → 字节流 → [存储/传输] → 字节流 → 反序列化 → 对象
                                                        
原对象和新对象是两个独立实例,只是状态一致
实例 字节流去了哪 用什么格式
游戏存档 磁盘文件 JSON
Socket 通信 网络管道 JSON
跨语言 从 Java 进程到 Python 进程 JSON

三个实例都用 JSON,是因为它可读、跨语言、示例清晰。实际工程中 RPC 内部通信更常用 Protobuf(更小更快),但原理完全一样。


四、序列化的本质:信息的保真转换

理解了场景,我们来提炼序列化的本质定义

序列化 = 将内存中对象的完整状态信息,转换成可传输/存储的字节流格式。

反序列化 = 从字节流中还原出原来的对象。

核心要求只有一个:保真

serialize(obj) 后再 deserialize(bytes),得到的新对象应该和原对象状态完全一致

User original = new User("张三", 28);
byte[] bytes = serialize(original);
User restored = deserialize(bytes);

// 保真要求:
assert restored.getName().equals(original.getName());  // 张三
assert restored.getAge() == original.getAge();         // 28
assert restored.equals(original);                      // 逻辑相等
// 注意:不是同一个对象!original != restored

4.1 保真的难题

"保真"说起来简单,但做起来有挑战:

难题 说明 例子
引用关系 对象 A 引用对象 B,序列化后还要还原这个引用 用户订单列表里的每个订单都要关联回原用户
循环引用 A 引用 B,B 又引用 A,怎么序列化? 用户与角色的双向引用
类型信息 反序列化时,要知道字节流对应什么类才能还原 字节流里要带上 com.example.User 的类名
状态 vs 行为 对象有状态(字段)和行为(方法),序列化只存状态 反序列化后,新对象的方法从哪来?类定义要存在
瞬态字段 有些字段不该序列化(如缓存、临时变量) transient 关键字标记
版本演进 类结构变了,旧数据反序列化可能失败 加了字段后,旧版本序列化的数据读不出来

五、常用序列化技术对比

序列化 ≠ JSON。JSON 只是序列化格式的一种。下面按流派系统对比。

5.1 流派划分

                    序列化技术
                       │
        ┌──────────────┼──────────────┐
        │              │              │
   Java 原生系      JSON 文本系    二进制跨语言系
        │              │              │
  Serializable     Jackson        Protobuf
  Externalizable   Gson           Thrift
                   Fastjson       MessagePack
                   Moshi          Hessian
                                  Avro
                                  Kryo

5.2 流派一:Java 原生序列化

JDK 自带,通过 java.io.Serializable 标记接口实现。

// 1. 类实现 Serializable 即可
public class User implements Serializable {
    private static final long serialVersionUID = 1L;  // 版本号
    private String name;
    private int age;
    private transient String password;  // 不序列化
}

// 2. 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"));
oos.writeObject(user);
oos.close();

// 3. 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"));
User user = (User) ois.readObject();
ois.close();

特点

维度 说明
优点 JDK 自带,无需依赖;使用极简;支持对象图(引用、循环引用)
缺点 只能 Java 用,跨语言差;体积大(带类信息);性能一般;安全漏洞多
安全 不安全的反序列化是经典攻击面(Log4j 漏洞就是一例)
适用 Java 内部的 RMI、Java 原生缓存、临时存储

ExternalizableSerializable 的子接口,需要自己实现 writeExternal / readExternal,灵活但繁琐,性能略好。

结论:新项目不要用 Java 原生序列化。它存在的意义主要是历史兼容。

5.3 流派二:JSON 文本系

把对象序列化成人类可读的文本格式(JSON 字符串),跨语言、可读性强。

5.3.1 Jackson

Spring 默认集成的 JSON 库,功能最全、生态最广。

ObjectMapper mapper = new ObjectMapper();

// 序列化
String json = mapper.writeValueAsString(user);
// {"name":"张三","age":28}

// 反序列化
User user = mapper.readValue(json, User.class);
维度 说明
优点 功能强大(注解、流式 API、树模型三套 API);性能好;生态丰富
缺点 API 相对重,配置项多,上手有点陡
适用 Spring 项目、REST API、通用 JSON 处理

5.3.2 Gson

Google 出品,轻量、API 简洁。

Gson gson = new Gson();
String json = gson.toJson(user);          // 序列化
User user = gson.fromJson(json, User.class);  // 反序列化
维度 说明
优点 极简 API;零依赖;对泛型支持好
缺点 性能不如 Jackson;扩展性弱
适用 Android、轻量场景、快速开发

5.3.3 Fastjson

阿里出品,性能优化激进,但历史上漏洞频发。

维度 说明
优点 性能极好(早期);API 简洁
缺点 历史漏洞多(多次 RCE);AutoType 机制是漏洞根源
适用 不推荐新项目使用;老项目可考虑迁移到 Fastjson2 或 Jackson

5.3.4 Moshi

Square 出品,Kotlin 友好,相对新潮。

维度 说明
优点 Kotlin 支持好;API 简洁;性能不错
缺点 生态不如 Jackson
适用 Kotlin/Android 项目

5.3.5 JSON 系的共同特性

共同点 说明
格式 文本,人类可读
跨语言 所有语言都支持
体积 比二进制大(字段名占空间)
性能 解析比二进制慢(要解析字符串)
Schema 无强类型 schema,结构靠字段名约定

5.4 流派三:二进制跨语言系

把对象序列化成紧凑的二进制字节流,跨语言、性能高、体积小。需要预定义 Schema(IDL,接口定义语言)。

5.4.1 Protobuf(Protocol Buffers)

Google 出品,业界事实标准,gRPC 的默认序列化协议。

// 1. 先定义 .proto schema
syntax = "proto3";
message User {
    string name = 1;
    int32 age = 2;
}
// 2. 编译生成 Java 类
// 3. 序列化/反序列化
User user = User.newBuilder().setName("张三").setAge(28).build();
byte[] bytes = user.toByteArray();           // 序列化
User restored = User.parseFrom(bytes);       // 反序列化
维度 说明
优点 体积最小;速度最快;跨语言;Schema 强类型;版本演进友好
缺点 不可读;需预编译;改字段要同步改 schema
适用 RPC(gRPC)、存储、高性能场景

5.4.2 Thrift

Facebook 出品,类似 Protobuf,但自带 RPC 框架。

维度 说明
优点 自带 RPC 框架;跨语言;性能好
缺点 文档少;社区活跃度不如 Protobuf
适用 旧系统、内部 RPC

5.4.3 MessagePack

"二进制 JSON",格式接近 JSON 但用二进制编码。

// 不需要 schema
ObjectMapper mapper = new ObjectMapper(new MessagePackFactory());
byte[] bytes = mapper.writeValueAsBytes(user);
User restored = mapper.readValue(bytes, User.class);
维度 说明
优点 比 JSON 小、快;无需 schema;API 接近 JSON
缺点 比 Protobuf 大、慢;不可读
适用 Redis 存储结构化数据、跨语言但不想定义 schema

5.4.4 Hessian

Caucho 出品,Dubbo 早期默认序列化协议。

维度 说明
优点 二进制、跨语言;自带 RPC 概念
缺点 性能不如 Protobuf;社区不活跃
适用 老版本 Dubbo

5.4.5 Avro

Apache 顶级项目,Hadoop/Kafka 生态常用。

维度 说明
优点 Schema 演进最好;大数据生态集成深
缺点 小项目偏重;学习成本高
适用 大数据场景(Kafka、Hadoop)

5.4.6 Kryo

Java 专用,不跨语言,性能极高。

维度 说明
优点 Java 内性能最好;体积小
缺点 不跨语言;不保证版本兼容
适用 纯 Java 内部的高性能序列化(如 Spark 内部、Flink 状态)

5.5 三大流派横向对比

维度 Java 原生 JSON 文本系 二进制跨语言系
跨语言 不支持 支持 支持
可读性 不可读 可读 不可读
体积 较大 最小
速度 较慢 最快
Schema 不需要 不需要 需要(除 MessagePack)
类型安全
版本演进
安全性 一般
典型代表 Serializable Jackson、Gson Protobuf、Avro

5.6 选型决策表

场景 推荐技术 原因
REST API、对外接口 Jackson(JSON) 可读、跨语言、生态好
内部 RPC、高性能通信 Protobuf + gRPC 体积小、速度快、类型安全
配置文件、简单存储 Jackson / Gson(JSON) 可读、易调试
缓存(Redis) Jackson(JSON)或 MessagePack JSON 可读,MessagePack 紧凑
大数据(Kafka、Hadoop) Avro Schema 演进友好、生态集成深
纯 Java 内部高性能 Kryo 同语言场景下最快
跨语言但不想定义 schema MessagePack 像 JSON 一样灵活,但更紧凑
历史遗留 Java RMI Java 原生 仅限兼容老系统

六、常见误解澄清

6.1 误解 1:"序列化就是把对象转成 JSON"

片面。JSON 只是序列化格式的一种。

序列化格式很多:

格式 类型 特点
Java 原生二进制 二进制 Java 专用、紧凑
JSON 文本 可读、跨语言、体积较大
Protobuf 二进制 高效、跨语言、需 schema
MessagePack 二进制 介于 JSON 和 Protobuf 之间
Avro 二进制 大数据生态常用
XML 文本 冗长、可读性好、成本高

序列化 ≠ JSON。JSON 是序列化的一种实现,序列化是一种思想。

6.2 误解 2:"序列化就是深拷贝"

不对

深拷贝:User → 直接在内存创建新的 User(同进程内)
序列化:User → 字节流 → 在另一个进程/机器创建新的 User

深拷贝在同一个进程内直接创建新对象,还是在内存里。序列化是跨进程/跨机器的行为,要经过字节流中转。

可以用序列化实现深拷贝(先序列化再反序列化),但序列化的目的不只是深拷贝。

6.3 误解 3:"序列化会序列化方法"

错误。序列化只存对象状态(字段值),不存方法

反序列化时,新对象的方法来自类定义——要求目标进程必须加载了对应的类。如果类不存在,反序列化就会失败(ClassNotFoundException)。

序列化的是:  对象的状态(字段值)
不序列化:    方法、类定义、static 字段、transient 字段
反序列化时:  方法从已加载的类定义中获取

6.4 误解 4:"JSON 比 Protobuf 通用,所以应该都用 JSON"

错误

JSON 通用是因为它可读、无 schema、跨语言——适合人机交互、对外 API

但在内部 RPC、高频通信、大数据存储场景:
- JSON 字段名占大量空间("username":"zhangsan"1:"zhangsan" 多 6 字节)
- JSON 解析慢(要解析字符串、转义、引号匹配)
- JSON 无类型约束(运行时才发现字段错了)

对外用 JSON,对内用 Protobuf——这是工业界的常见做法。

6.5 误解 5:"反序列化就是 new 一个对象然后赋值"

不准确

反序列化创建对象的方式绕过了构造方法

// 普通创建对象:调用构造方法
User u = new User("张三", 28);

// 反序列化:通过反射/unsafe 直接分配内存,不调用构造方法
User u = (User) unsafe.allocateInstance(User.class);
// 然后通过反射给字段赋值

这就是为什么:
- 反序列化时构造方法不会执行(单例会被破坏)
- final 字段在反序列化时可能被绕过
- 单例类要实现 readResolve() 防止反序列化破坏单例


七、序列化的代价

任何技术都有代价,序列化也不例外:

代价 说明
性能开销 序列化/反序列化需要 CPU,格式选型影响速度
体积膨胀 对象变字节后,往往比直接在内存中占用更大
版本兼容 类结构变了,旧数据反序列化可能失败
安全风险 不安全的反序列化是常见漏洞(Log4j 就是反序列化漏洞)
类型耦合 原生 Java 序列化绑定了 Java 版本,跨语言困难

八、学习路径建议

第一阶段:建立概念
├── 理解序列化的本质(对象与字节流的转换)
├── 搞懂三大场景(持久化、通信、跨语言)
└── 写出第一个 Serializable 程序

第二阶段:掌握工具
├── Jackson / Gson(JSON 处理)
├── Java 原生序列化的机制与坑
├── transient、serialVersionUID 的作用
└── readResolve / writeReplace 的原理

第三阶段:深入原理
├── 二进制序列化格式的设计(Protobuf 编码原理)
├── Schema 演进与版本兼容
├── 反序列化绕过构造方法的机制
└── 序列化安全漏洞的攻击面与防御

第四阶段:工程实践
├── RPC 框架的序列化集成
├── 大规模数据的序列化选型
├── 跨语言通信的 schema 管理
└── 序列化性能调优

九、总结

序列化的根本动机,是为了跨越"内存世界"和"外部世界"的鸿沟。

内存里的对象有类、有引用、有生命周期,一旦要持久化、要传输、要跨语言交流,就必须把它"翻译"成中立的字节流——这就是序列化。

反序列化是反向过程:从字节流重新构造出对象。新对象和原对象不是同一个实例,只是状态一致——所以序列化本质是复制,不是共享

技术选型上:
- 对外、可读、跨语言 → JSON(Jackson)
- 对内、高性能、跨语言 → Protobuf(gRPC)
- 大数据、Schema 演进 → Avro
- 纯 Java 内部极致性能 → Kryo

记住一句话:序列化不是为了"变成字节",而是为了"跨越边界"——让对象的状态能穿越进程、机器、语言的隔阂。