如果说Parcelable是靠 “手动标刻度、按顺序摆” 的精密打包,那Serializable就是一位 “带自动清单” 的佛系快递员 —— 它不用你精确计算每个物品的体积,而是会在包裹里塞一张详细的 “物品说明书”(类元数据),里面写着每个物品的名字、类型甚至摆放顺序。靠着这张说明书,拆包时哪怕顺序有点小偏差(只要名字对得上),也能把东西归位。但这 “佛系” 的背后,藏着一套自动记录 “物品位置” 和 “总大小” 的巧妙逻辑。
为啥 Serializable 不慌不忙?因为有 “自动清单”
想象你要寄一个 “杂物箱”(对象),里面有:一把钥匙(int)、一张纸条(String)、一个旧钱包(另一个Serializable对象)。Serializable快递员的做法是:
-
不要求你按固定顺序摆, but 会在箱子里放一张清单,写着 “钥匙在左边(字段名
key)、纸条在中间(字段名note)、钱包在右边(字段名wallet)”; -
每个物品的 “体积”(
size)会被自动测量并记在清单上,比如钥匙占 4cm³,纸条占 “长度 + 内容” 的体积,钱包的体积是它内部所有物品的总和; -
拆包时,快递员会先看清单,按 “名字” 找物品,再按记录的 “体积” 取,哪怕你寄的时候随手乱摆,只要清单没丢,就能对应上。
这张 “清单” 就是Serializable底层的类元数据(包含类名、字段名、类型、serialVersionUID等),正是它替代了Parcelable的 “手动指针”,成为保证字段正确存取的核心。
第一幕:打包时的 “自动清单生成” 与 “体积累加”(序列化)
Serializable的打包靠ObjectOutputStream(打包机)完成,整个过程就像给杂物箱 “拍 X 光”,自动记录每个物品的信息和体积。
步骤 1:给箱子贴 “身份标签”(类元数据写入)
当你把对象放进打包机:
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("box.txt"));
out.writeObject(杂物箱); // 开始打包
打包机先做的事,是给箱子贴标签:
-
写上箱子的 “品牌”(类的全限定名,比如
com.example.MyBox); -
盖上 “防伪码”(
serialVersionUID,如果没手动指定,会根据类结构自动生成)—— 这是为了告诉拆包员 “这箱子和你那边的型号匹配”; -
列出箱子里所有 “非隐藏” 物品(非
transient字段)的名字和类型,比如 “key(int)、note(String)、wallet(Wallet)”。
这一步写入的元数据会占用一部分体积,就像清单本身需要一张纸的空间。
步骤 2:按 “字段顺序” 测体积、写物品,自动累加 size
虽然Serializable不要求你手动控制顺序,但打包机默认会按类中字段声明的顺序来处理物品(父类字段先于子类,同级别按代码顺序),并自动计算每个物品的体积,累加到总size里。
不同物品的 “体积计算规则” 如下:
| 物品类型(数据类型) | 体积(size)计算方式 | 打包机的操作(序列化逻辑) |
|---|---|---|
| 基本类型(int、boolean 等) | 固定体积(如 int 占 4 字节,boolean 占 1 字节) | 直接写入值,并在清单上记 “key(int):体积 4 字节,位置从 X 到 X+4” |
| String(字符串) | 4 字节(存长度) + 字符串实际字节数(UTF-8 编码) | 先写长度,再写内容,体积记为 “4 + 内容长度”,清单上标注 “note(String):体积 Y 字节” |
| 其他 Serializable 对象(如 Wallet) | 该对象的元数据体积 + 其内部所有字段的体积总和 | 先写这个对象的元数据(如果是第一次出现),再递归写入其内部字段,体积累加,清单上记 “wallet(Wallet):总容积 Z 字节” |
| transient 字段(如 “私房钱”) | 不计算体积,不写入打包机 | 清单上会注明 “secretMoney(transient):忽略”,相当于这些物品被打包机 “视而不见” |
| 重复出现的对象(如两个引用指向同一个钱包) | 只算一次完整体积,后续重复引用只记 “引用 ID”(4 字节) | 第一次写钱包时算全体积,第二次遇到同一个钱包,只在清单上记 “wallet2:引用 ID=1”(指向第一次的位置) |
举个例子:打包 “杂物箱(MyBox)”
MyBox有 3 个字段:int key = 123、String note = "hello"、Wallet wallet = new Wallet(100)(Wallet有一个int money字段)。
| 处理步骤 | 写入内容 | 体积(size)计算 | 总 size 累计 |
|---|---|---|---|
| 1. 写入 MyBox 元数据 | 类名、serialVersionUID、字段列表(key、note、wallet) | 约 100 字节(元数据固定开销) | 100 |
| 2. 写入 key(int) | 值 123 | 4 字节 | 104 |
| 3. 写入 note(String) | 长度 5("hello" 是 5 个字符) + 内容字节 | 4(长度)+5=9 字节 | 113 |
| 4. 写入 Wallet 元数据 | 类名、serialVersionUID、字段 money | 约 80 字节 | 193 |
| 5. 写入 wallet.money(int) | 值 100 | 4 字节 | 197 |
最终整个包裹的总size是 197 字节,这些数据会被连续写入磁盘 / 内存,就像箱子被填满后,总容积就是所有物品(包括清单)的体积总和。
关键:体积自动累加,位置靠 “偏移量” 记录
打包机内部有一个 “当前位置指针”(类似Parcel的mDataPos),但这个指针是自动移动的:
-
每写入一个数据,指针就加上该数据的体积(比如写完 4 字节的 int,指针 + 4);
-
所有数据按写入顺序连续存储,每个字段的 “位置” 就是它在总字节流中的偏移量(从 0 开始算,比如 key 从 100→104,note 从 104→113)。
清单上虽然不直接写偏移量,但拆包时会通过 “总字节流 + 体积累加” 反推每个字段的位置 —— 就像知道 “钥匙占 4cm³,放在清单后第 0-4cm 处,纸条占 9cm³,放在 4-13cm 处”。
第二幕:包裹上路,清单和物品同路
打包好的包裹(序列化后的字节流)会被存储(文件)或传输(网络),里面的 “清单 + 物品 + 体积记录” 完整保存,就像箱子连带着清单一起被运走,中途不会丢失任何信息。
第三幕:拆包时的 “按清单找物”(反序列化)
拆包靠ObjectInputStream(拆包机),它的核心动作是 “读清单→按名找物→按体积取物”,哪怕字段顺序和打包时不一样,只要名字和类型对得上,就能正确还原。
还是用上面的 “杂物箱” 举例:
-
拆包机先读 “清单”(元数据),知道这是
MyBox类,有key(int)、note(String)、wallet(Wallet)三个字段,还有serialVersionUID(验证型号匹配); -
按 “清单顺序” 或 “字段名匹配” 读取物品:
- 先找
key:根据清单记录的 “int 类型,体积 4 字节”,从当前指针位置(0)读 4 字节,得到 123,指针 + 4; - 再找
note:清单说 “String 类型,体积 9 字节(4+5)”,先读 4 字节得长度 5,再读 5 字节得 “hello”,指针 + 9; - 最后找
wallet:先读Wallet的元数据(验证是同一个类),再按其内部清单读money(4 字节,100),指针累加对应体积;
- 先找
-
所有字段读完后,拆包机用反射创建
MyBox对象,把读到的值按 “字段名” 赋值(比如myBox.key = 123),完成还原。
为什么位置不会错?靠 “偏移量 + 清单校验”
Serializable不需要手动控制指针,但底层靠 “字节流偏移量” 和 “清单记录的体积” 保证位置正确:
- 每个字段的 “起始位置”= 前一个字段的起始位置 + 前一个字段的体积;
- 拆包时,拆包机严格按 “打包时的体积” 读取,比如知道
key占 4 字节,就绝不会多 read 1 字节,确保下一个字段的起始位置准确; - 清单上的 “字段名 + 类型” 是双重保险:如果打包时字段顺序是
key→note,拆包时类的字段顺序是note→key,只要名字对得上,拆包机也能通过反射按名赋值(但建议顺序一致,避免兼容问题)。
总结:Serializable 的 “佛系” 背后是 “自动记账”
Serializable之所以能保证字段正确存取、size 计算无误,核心靠三点:
-
元数据清单:记录类信息、字段名、类型,解决 “找谁” 的问题;
-
体积自动累加:每个字段的 size 按类型规则计算,指针随写入自动偏移,解决 “在哪” 的问题;
-
引用机制:重复对象只记一次体积,后续用 ID 引用,解决 “重复计算” 的问题。
它不像Parcelable那样靠 “人工精确控制”,而是靠 “自动记录 + 清单校验”,虽然慢一点(元数据占空间、反射耗时间),但胜在简单 —— 你只需要给类加个implements Serializable,剩下的打包、算体积、记位置,全交给快递员就好。
下次用Serializable时,不妨想象它在默默写清单的样子:“这个字段叫啥,占多大地方,下一个该从哪开始……” 正是这些细节,让乱乱的包裹也能被完美还原。