目录
  1. 1. 一、什么是序列化
  2. 2. 二、Java Serializable
    1. 2.1. 2.1 基本使用
    2. 2.2. 2.2 serialVersionUID 详解
    3. 2.3. 2.3 transient 关键字
    4. 2.4. 2.4 自定义序列化:writeObject/readObject
    5. 2.5. 2.5 Externalizable 接口
    6. 2.6. 2.6 序列化格式分析
    7. 2.7. 2.7 序列化安全问题
  3. 3. 三、Android Parcelable
    1. 3.1. 3.1 为什么需要 Parcelable
    2. 3.2. 3.2 Parcelable 实现
    3. 3.3. 3.3 Parcelable 的性能优势
    4. 3.4. 3.4 使用 Kotlin 的 @Parcelize
  4. 4. 四、JSON 序列化
    1. 4.1. 4.1 Gson 原理
    2. 4.2. 4.2 Moshi vs Gson vs kotlinx.serialization
    3. 4.3. 4.3 FastJson 的安全性问题
  5. 5. 五、Protobuf(Protocol Buffers)
    1. 5.1. 5.1 定义消息格式
    2. 5.2. 5.2 编码格式
    3. 5.3. 5.3 Protobuf vs JSON 对比
  6. 6. 六、各序列化方案的适用场景
  7. 7. 七、源码路径
  8. 8. 八、常见面试题
Java进阶之序列化

一、什么是序列化

序列化(Serialization)是将对象的状态信息转换为可存储或传输的形式的过程。反序列化(Deserialization)是其逆过程——将字节序列恢复为对象。

在 Java 生态中,序列化主要应用于以下场景:

  • 进程间通信(IPC):Android 的 Intent 通过 Binder 传递数据,Binder 使用 Parcelable 将对象序列化后跨进程传输。
  • 持久化存储:将对象保存到文件(如 SharedPreferences、SQLite、文件缓存)。
  • 网络传输:将对象转换为 JSON/XML/Protobuf 等格式发送到服务端。
  • 深拷贝:通过序列化-反序列化实现对象的完整克隆。

二、Java Serializable

2.1 基本使用

import java.io.Serializable;

public class User implements Serializable {
// 序列化版本号——反序列化时用于验证版本一致性
private static final long serialVersionUID = 1L;

private String name;
private int age;
private transient String password; // transient 字段不会被序列化

// 构造方法(反序列化时不会调用!)
public User(String name, int age) {
this.name = name;
this.age = age;
}
}

// 序列化
User user = new User("张三", 25);
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.ser"))) {
oos.writeObject(user);
}

// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("user.ser"))) {
User restored = (User) ois.readObject();
// 注意:反序列化时构造方法不会被调用!
}

2.2 serialVersionUID 详解

serialVersionUID 是序列化版本控制的关键。它的作用是:

  1. 序列化时,对象的 serialVersionUID 被写入序列化流
  2. 反序列化时,JVM 将流中的 serialVersionUID 与当前类的 serialVersionUID 比较
  3. 如果两者不一致,抛出 InvalidClassException

如果未显式声明 serialVersionUID?
JVM 会根据类的结构(类名、接口、字段、方法签名等)自动计算一个哈希值作为默认的 serialVersionUID。问题是这个哈希值对类结构的变化极其敏感——哪怕增加一个空格或注释(虽然在字节码层面注释不影响),增加或删除一个字段都会导致新哈希值不同,从而无法反序列化旧版本的序列化数据。

最佳实践:始终显式声明 serialVersionUID,并在类结构调整时手动更新:

private static final long serialVersionUID = 20200101L;  // 使用日期作为版本号

2.3 transient 关键字

transient 修饰的字段不会被默认序列化机制写入流中。典型使用场景:

public class UserSession implements Serializable {
private static final long serialVersionUID = 1L;

private String username;
private transient String password; // 密码不序列化(安全考虑)
private transient long lastLoginTime; // 登录时间不需要持久化
private transient ThreadLocal<String> context; // ThreadLocal 无法序列化
}

对于 transient 字段,反序列化后其值为类型的默认值(引用类型为 null,int 为 0)。

2.4 自定义序列化:writeObject/readObject

通过定义私有的 writeObjectreadObject 方法,可以自定义序列化行为:

public class SecureUser implements Serializable {
private static final long serialVersionUID = 1L;

private String username;
private transient String encryptedPassword;

// 自定义序列化
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 先执行默认序列化
// 对密码字段做额外加密
String extraEncrypted = Base64.getEncoder()
.encodeToString(encryptedPassword.getBytes());
oos.writeObject(extraEncrypted);
}

// 自定义反序列化
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // 先执行默认反序列化
// 解密密码字段
String encrypted = (String) ois.readObject();
this.encryptedPassword = new String(
Base64.getDecoder().decode(encrypted));
}
}

调用时机

  • writeObjectObjectOutputStream.writeObject() 内部通过反射调用
  • readObjectObjectInputStream.readObject() 内部通过反射调用

2.5 Externalizable 接口

Externalizable 继承自 Serializable,它要求开发者完全手动控制序列化逻辑:

public class Product implements Externalizable {
private static final long serialVersionUID = 1L;

private String name;
private double price;

// 必须有一个 public 无参构造函数(Serializable 不需要)
public Product() {}

public Product(String name, double price) {
this.name = name;
this.price = price;
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeDouble(price);
}

@Override
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException {
name = in.readUTF();
price = in.readDouble();
}
}

Externalizable vs Serializable

维度 Serializable Externalizable
实现方式 JVM 自动处理 完全手动
性能 使用反射,较慢 无反射,快
控制粒度 通过 transient 排除字段 精确控制每个字段
构造方法 不调用构造方法 必须有无参 public 构造方法
序列化数据量 包含类的元信息,较大 只包含你写的数据,更小

2.6 序列化格式分析

Java 序列化产生的二进制格式遵循特定规范。以序列化一个简单的 User 对象为例:

偏移量  Hex                          说明
------ --- ----
0x0000 AC ED STREAM_MAGIC (魔术数字)
0x0002 00 05 STREAM_VERSION (版本号 5)
0x0004 73 TC_OBJECT (对象标记)
0x0005 72 00 04 55 73 65 72 TC_CLASSDESC + 类名 "User"
0x000C 00 00 00 00 serialVersionUID (0L)
0x0010 02 标志位 (SC_SERIALIZABLE)
0x0011 00 02 字段数量 = 2
0x0013 49 00 03 61 67 65 字段1: I(int) + "age"
0x0019 4C 00 04 6E 61 6D 65 字段2: L(reference) + "name"
0x0020 00 12 4C 6A 61 76 61 2F ... 类型: "Ljava/lang/String;"
0x0036 78 TC_ENDBLOCKDATA
... (对象数据部分)

格式分析可以从 ObjectInputStream 源码中找到完整的解析逻辑。HotSpot 中相关的 native 方法在 src/java.base/share/native/libjava/ObjectInputStream.c 中。

2.7 序列化安全问题

Java 序列化有严重的安全隐患,是许多远程代码执行(RCE)漏洞的根源:

// 反序列化漏洞示例
// 攻击者构造特殊的序列化数据,触发链式调用,最终执行恶意代码
ObjectInputStream ois = new ObjectInputStream(untrustedInput);
Object obj = ois.readObject(); // 危险!可能执行恶意代码

防护措施

  1. 使用 ObjectInputFilter(JDK 9+)限制可反序列化的类
  2. 避免使用 Java 原生序列化传输不可信数据
  3. 优先使用 JSON、Protobuf 等安全的序列化格式
  4. 在 Android 中,跨进程通信使用 Parcelable(类型安全,不会导致任意代码执行)

三、Android Parcelable

3.1 为什么需要 Parcelable

在 Android 中,跨进程传输数据主要通过 Binder 机制。Binder 的事务缓冲区只有 1MB(并且是所有 Binder 调用共享的),因此需要一种内存高效、速度快的序列化方式。Serializable 使用反射 + 大量元数据,序列化后的数据体积大、速度慢,不适合 Binder 传输。

Parcelable 专为 Android IPC 设计:

  • 零反射:所有序列化逻辑由开发者显式编写(或通过 APT 生成)
  • 紧凑格式:不存储类名、字段名等元信息,只存数据
  • 与 Binder 深度集成:Parcel 是 Binder 通信的直接载体

3.2 Parcelable 实现

public class User implements Parcelable {
private String name;
private int age;
private List<String> hobbies;

// 构造方法
public User(String name, int age) {
this.name = name;
this.age = age;
}

// 从 Parcel 中反序列化
protected User(Parcel in) {
name = in.readString();
age = in.readInt();
hobbies = new ArrayList<>();
in.readStringList(hobbies);
}

// 序列化到 Parcel
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(age);
dest.writeStringList(hobbies);
}

// 描述内容(通常返回 0,有文件描述符时返回 CONTENTS_FILE_DESCRIPTOR)
@Override
public int describeContents() {
return 0;
}

// CREATOR 是反序列化的工厂
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}

@Override
public User[] newArray(int size) {
return new User[size];
}
};
}

注意writeToParcel 和 CREATOR 的 createFromParcel 中的读写顺序必须严格一致——这是手动序列化最大的坑。

3.3 Parcelable 的性能优势

关于 “Parcelable 比 Serializable 快 10 倍” 的说法:

指标 Serializable Parcelable
序列化方式 反射(慢) 手写代码(快)
数据体积 类名、字段名等元信息 仅数据值
对象创建 不经过构造方法 经过构造方法
跨平台兼容 JVM 通用 Android 专有
版本兼容 serialVersionUID 机制 需自己处理

实际测试数据(简单对象,10000 次序列化):

  • Serializable: ~300ms
  • Parcelable: ~20ms
  • 性能差距约 10-15 倍

3.4 使用 Kotlin 的 @Parcelize

Kotlin 提供了 @Parcelize 注解,通过编译器插件自动生成 Parcelable 实现,无需手写繁琐的 writeToParcel:

import kotlinx.parcelize.Parcelize

@Parcelize
data class User(
val name: String,
val age: Int,
val hobbies: List<String> = emptyList()
) : Parcelable

Kotlin 编译器会在 class 文件中自动生成 writeToParcelCREATOR 的实现。这是 Android 上最推荐的 Parcelable 实现方式。

四、JSON 序列化

4.1 Gson 原理

Gson 是 Google 开发的 Java JSON 序列化库。其核心原理是通过反射读取对象的字段,然后递归序列化:

// 序列化
User user = new User("张三", 25);
Gson gson = new Gson();
String json = gson.toJson(user);
// {"name":"张三","age":25}

// 反序列化
User restored = gson.fromJson(json, User.class);

Gson 的内部机制

  1. toJson() → 获取对象的 TypeToken → 创建 TypeAdapter
  2. TypeAdapter 通过反射(Field.get())读取每个字段的值
  3. 对于复杂字段(嵌套对象、集合),递归创建子 TypeAdapter
  4. 最终通过 JsonWriter 输出 JSON 字符串

Gson 的类型擦除问题

由于 Java 泛型擦除,以下代码会有问题:

// 错误!反序列化后得到的是 LinkedTreeMap 而非 User
List<User> users = gson.fromJson(json, List.class);

// 正确:使用 TypeToken 保留泛型信息
Type listType = new TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, listType);

4.2 Moshi vs Gson vs kotlinx.serialization

维度 Gson Moshi kotlinx.serialization
反射 使用反射 使用反射 + CodeGen 编译期代码生成(无反射)
Kotlin 支持 一般 (需额外处理 null safety) 好 (Kotlin 感知) 原生 Kotlin
性能 中 (CodeGen 快) 快(无反射)
Android 推荐 传统项目 OkHttp 系列项目 现代 Kotlin 项目

为什么 kotlinx.serialization 最快?

它完全在编译期生成序列化代码,运行时零反射:

@Serializable
data class User(
val name: String,
val age: Int
)

// 编译后自动生成序列化器 User$$serializer
// 序列化时直接调用生成的代码,无反射开销
val json = Json.encodeToString(User.serializer(), user)

4.3 FastJson 的安全性问题

FastJson 是阿里巴巴开源的 JSON 库,在早期因其高性能被广泛使用。但它存在严重的安全漏洞(反序列化 RCE):

  • 通过 @type 字段指定类名,FastJson 会尝试实例化任意类
  • 结合特定类的 setter/getter 方法链,可以触发任意代码执行
  • 修复方案:升级到最新版本、开启 SafeMode、使用 autoType 白名单

Android 开发建议:优先使用 Gson(稳定)、Moshi(Kotlin)、kotlinx.serialization(现代 Kotlin)。

五、Protobuf(Protocol Buffers)

Protobuf 是 Google 开发的语言中立、平台中立的可扩展序列化格式。相比 JSON,它有几个显著优势:

5.1 定义消息格式

// user.proto
syntax = "proto3";

message User {
string name = 1;
int32 age = 2;
repeated string hobbies = 3;

enum Gender {
UNKNOWN = 0;
MALE = 1;
FEMALE = 2;
}
Gender gender = 4;
}

5.2 编码格式

Protobuf 使用 Tag-Length-Value (TLV) 编码:

字段编码 = (field_number << 3) | wire_type

Wire Types:
0 = Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum)
1 = 64-bit (fixed64, sfixed64, double)
2 = Length-delimited (string, bytes, embedded messages, packed repeated fields)
3 = Start group (deprecated)
4 = End group (deprecated)
5 = 32-bit (fixed32, sfixed32, float)

Varint 编码:一种变长整数编码,小数字使用更少的字节:

数字 300 的 Varint 编码:
300 = 0b100101100
→ 按 7 位分组: 0000010 | 0101100
→ 小端序,高位组加 continuation bit (1): 10101100 00000010
→ 两个字节: 0xAC 0x02

对比:
fixed32: 0x2C 0x01 0x00 0x00 (4 字节)
Varint: 0xAC 0x02 (2 字节)

5.3 Protobuf vs JSON 对比

维度 JSON Protobuf
可读性 人可读 二进制,不可读
体积 较大(字段名重复) 小(字段序号代替字段名)
解析速度 慢(文本解析) 快(二进制解析)
Schema 无(弱类型) 有(强类型,通过 .proto 定义)
版本兼容 手动处理 内置向前/向后兼容
Android 支持 全平台 需要 proto 编译器和运行时
适用场景 REST API、配置文件 RPC(gRPC)、高性能存储

六、各序列化方案的适用场景

场景 推荐方案 原因
Intent/Bundle 传递数据 Parcelable 与 Binder 集成,速度快
网络 API 通信 JSON (Moshi/Gson) 人可读,跨语言
微服务 RPC 通信 Protobuf + gRPC 高性能,强类型
本地持久化(简单) JSON / SharedPreferences 实现简单
本地持久化(大量) Protobuf / Room (SQLite) 读写入性能好
缓存数据 Serialization (LruCache 应用类) 深度拷贝方便
对象深拷贝 Serialization 一行代码完成
跨进程大数据 Parcelable + ContentProvider Binder 限制 1MB

七、源码路径

组件 路径
Java Serializable (JDK) jdk/src/java.base/share/classes/java/io/ObjectOutputStream.java
Parcelable frameworks/base/core/java/android/os/Parcelable.java
Parcel (native) frameworks/native/libs/binder/Parcel.cpp
Parcel (Java) frameworks/base/core/java/android/os/Parcel.java
Gson gson/gson/src/main/java/com/google/gson/
kotlinx.serialization kotlin/kotlinx.serialization/

八、常见面试题

Q1: Parcelable 和 Serializable 有什么区别?为什么 Android 推荐使用 Parcelable?

A: 核心区别:Serializable 是 Java 标准序列化机制,使用反射自动序列化对象的所有字段,序列化后的数据包含类名、字段名等元信息,体积大且速度慢。Parcelable 是 Android 专有的序列化接口,要求开发者显式编写序列化代码,序列化后的数据只包含字段值,不含元信息,因此体积更小、速度更快(通常快 10-15 倍)。Android 推荐 Parcelable 的原因是:(1) Binder 的事务缓冲区只有 1MB,Parcelable 的紧凑格式更适合;(2) Parcelable 不经过反射,性能更好;(3) 跨进程通信是 Android 应用的核心场景(Activity 跳转、Service 绑定等),Parcelable 与 Binder 深度集成。缺点是 Parcelable 需要手写大量样板代码(Kotlin 的 @Parcelize 解决了这个问题)。

Q2: serialVersionUID 的作用是什么?不设置会怎样?

A: serialVersionUID 是序列化版本控制的标识符。序列化时,它会随对象数据一同写入流中。反序列化时,JVM 会比较流中的 serialVersionUID 与本地类的 serialVersionUID,如果不一致则抛出 InvalidClassException。如果不设置,JVM 会根据类的结构(类名、字段、方法签名等)通过 SHA 算法自动生成一个。问题在于自动生成的值对类结构非常敏感——增加一个字段、改变一个方法签名都会导致生成的 serialVersionUID 不同,从而导致旧版本序列化的数据无法反序列化。因此最佳实践是显式声明一个 serialVersionUID 并在类演化时保持兼容性。

Q3: 什么是 transient 关键字?反序列化后 transient 字段的值是什么?

A: transient 是 Java 的关键字,用于标记不需要序列化的字段。被标记的字段在序列化时会被忽略,反序列化后其值为该类型的默认值:引用类型为 null,int/short/byte/long/float/double 为 0,boolean 为 false,char 为 ‘’。典型使用场景:(1) 安全敏感数据(密码、Token);(2) 运行时状态(ThreadLocal、当前登录状态);(3) 可重新计算的派生字段(缓存值、聚合结果);(4) 不可序列化的字段(如 Context、View 等 Android 组件)。如果需要给 transient 字段赋予合理的反序列化值,可以在 readObject() 方法中手动处理。

Q4: Binder 的事务缓冲区为什么只有 1MB?传输大数据时怎么办?

A: Binder 的 1MB 限制是 Android 系统设计的一个安全阈值(定义在 frameworks/native/libs/binder/ProcessState.cpp 中的 BINDER_VM_SIZE),目的是防止单个 Binder 调用占用过多内核内存影响系统稳定性。当需要传输大数据时:(1) 对于图片/文件,使用 ContentProvider + 文件描述符(通过 Parcel 发送 ParcelFileDescriptor);(2) 对于大量结构化数据,使用 ContentProvider + Cursor;(3) 使用 MemoryFile(匿名共享内存,ashmem)共享大块数据,通过 Binder 只传递文件描述符;(4) 使用 Messenger + Bundle(Binder 限制的微妙差异,Bundle 的 putBinder 没有 1MB 限制但也不推荐大数据)。注意:1MB 是所有并发 Binder 调用共享的,实际可用空间可能小于 1MB,如果同时有多个 Binder 调用在进行,你的可用缓冲区会更小。

Q5: Gson 如何处理泛型?为什么 TypeToken 能保留泛型信息而直接传 Class 不能?

A: Java 的泛型在编译期会被擦除(Type Erasure),List<User>.class 在运行时不存在,它只是 List.class。因此在 gson.fromJson(json, List.class) 中,Gson 只能知道这是一个 List,但不知道 List 的元素类型,只能默认为 LinkedTreeMap(Map 类型)。TypeToken 使用了匿名内部类的技巧:new TypeToken<List<User>>(){} 创建了一个匿名子类,JVM 保留了其父类的泛型签名在 class 文件的 Signature 属性中(虽然字节码层面泛型被擦除,但 Signature 属性保留了完整的泛型信息)。Gson 通过 getGenericSuperclass() 读取这个 Signature 属性,获取到完整的 List<User> 类型信息。这就是 TypeToken 能够保留泛型信息的原理。

Q6: 反序列化漏洞(RCE)的原理是什么?Android 中是否存在类似的风险?

A: Java 反序列化 RCE 的典型攻击链:攻击者构造一个特殊的序列化字节流,其中包含一个精心设计的对象图。当 readObject() 被调用时,JVM 按照字节流中的指令逐步创建对象并调用它们的 readObjectreadResolve 等方法。如果某个类的这些方法中存在危险的逻辑(如执行系统命令、加载远程类、调用 Runtime.exec()),就会形成攻击链。著名的利用链包括 Commons Collections、Spring Beans、JDK 内置的 AnnotationInvocationHandler 等。在 Android 中,原生序列化(Serializable)的风险较小,因为:(1) Android 不包含 Java EE 的那些危险类(如 Commons Collections);(2) Binder 的 Parcelable 是类型安全的——deserialize 时明确指定了类(通过 CREATOR),不会像 Java 原生序列化那样从流中动态加载任意类;(3) Android 9+ 对 ObjectInputStream 增加了额外的安全检查。但不意味着绝对安全——如果你的应用使用了存在漏洞的第三方库,仍可能受到影响。


参考文档:

打赏
  • 微信
  • 支付宝

评论