序列化 = 把整个对象打包成字节序列写进文件(ObjectOutputStream.writeObject),反序列化 = 从文件拆包还原成对象(ObjectInputStream.readObject)——不用一个字段一个字段抠,整只快递寄走、原样签收。
为什么需要它:存对象的麻烦
10.2~10.4 学的都是「拆碎了搬」:存一个学生,得自己拼 name=波仔,age=18 写出去,读回来再 split、再 new、再 set——字段一多就是体力活,还容易拼错。
序列化的思路:别拆,整只寄。对象在内存里是一坨完整数据,把它原样变成字节序列写进文件;要用的时候,从文件里读回字节序列,原地还原成一个一模一样的对象。字段名字、类型、值全都在,不用你操心怎么拼怎么拆。
两个方向(口诀还是 10.2 的:以内存为家):
| 动作 | 类 | 方法 | 方向 |
|---|---|---|---|
| 序列化 | ObjectOutputStream | writeObject(对象) |
内存 → 文件 |
| 反序列化 | ObjectInputStream | readObject() |
文件 → 内存 |
Object 开头的这两个类是字节流(OutputStream/InputStream 家族),不是字符流——因为它搬运的是字节序列,里面可没有"文本"概念。构造时底层还是套 FileOutputStream / FileInputStream,老套路:翻译官/快递员自己不搬,雇字节搬运工在下面。
五条保命规矩
① 准入证:类必须实现 Serializable
public class Student implements Serializable {
// 一个方法都不用写——它是标记接口
}
不实现就 writeObject?直接炸:NotSerializableException(运行时)。
回想 4.3 讲接口:接口一般是"合同",要实现方法。但 Serializable 里一个方法都没有——这种空接口叫标记接口(Marker Interface),它的作用不是给方法,而是给 JVM 看:这个类授权被打包。当初 4.3 埋的"接口还有一种玩法",这里收网。
② 身份证:serialVersionUID
public class Student implements Serializable {
@java.io.Serial
private static final long serialVersionUID = 1L;
}
这是打包时附在快递单上的版本号。拆包时 JVM 会核对:文件里的版本号和当前代码里的类版本号一致才肯还原。
不写行不行? 能跑——但类一旦改动(加个字段、改个方法),JVM 会按类的结构自动算一个新版本号,和文件里的旧号对不上,读的时候炸:InvalidClassException: local class incompatible。写了固定值(如 1L),类小改也能兼容读回来。所以规矩是:凡是 implements Serializable,就顺手写上 serialVersionUID。
③ 红牌:transient 瞬态关键字
private String password; // 会被打包——不安全!
private transient String password; // 红牌:不参与序列化
某些字段不想进文件:密码、银行卡号(敏感),或者缓存、大图(没必要)。在字段前加 transient,打包时直接跳过。读回来这个字段是默认值:引用型 null、数值型 0、boolean false——正好是 2.x 讲的成员变量默认值那张表。
④ static 字段不上车
序列化打包的是对象(一堆实例字段的值),而 static 字段属于类,全类共享一份——所以 static 不参与序列化。就算你把 static int school = "一中" 改了再读,读出来的也跟文件无关,反序列化后看到的是当前 JVM 里 static 的值。别指望用序列化保存 static 数据。
⑤ readObject 返回 Object,必须强转
Student s = (Student) ois.readObject();
JVM 拆包时不知道你要什么类型,统一返回 Object——4.4 多态「编译看左运行看右」的旧账,向下转型要自己强转。
另外注意:readObject() 会抛两个受检异常——IOException(读文件本身)和 ClassNotFoundException(拆包要按文件里记录的类名找对应的类,找不到就炸——9.1 反射章 Class.forName 的同款异常,同一条根:按名字找类,名字失效就找不着)。
Demo09:寄出与签收
public class Demo09 {
public static void main(String[] args) throws Exception {
// 第一段:序列化——把对象寄出去
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("day11\\stu.txt"))) {
Student stu = new Student("波仔", 18);
oos.writeObject(stu);
}
// 第二段:反序列化——拆包还原
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("day11\\stu.txt"))) {
Student s = (Student) ois.readObject();
System.out.println(s.getName() + " " + s.getAge()); // 波仔 18
}
}
}
- 写出后打开
stu.txt用记事本看:一团乱码——正常!里面是字节序列不是文本,readObject才认识它。千万别手贱去改它 - 两次用完都关流——try-with-resources 包办(10.2 的姿势)
把 implements Serializable 删掉再 writeObject → NotSerializableException: Student。异常名直接告诉你谁没资格。
先序列化一次(不写 serialVersionUID),然后给 Student 加个字段 double score,再运行反序列化那段 → InvalidClassException: local class incompatible。补上 serialVersionUID = 1L 固定值再测——加字段也能读回来。
password 加 transient,序列化时设密码 "123456",反序列化后打印 → null。把类型换成 int 再试 → 0。
常见误区
- 「序列化后的文件是文本,能用记事本编辑」——不是,是字节序列。记事本打开是乱码,手改一个字节拆包就炸(版本核对/结构错乱)。
- 「实现了 Serializable 就万事大吉」——还得写 serialVersionUID,否则类一改旧文件全废。版本号是给自己留的后路。
- 「transient 修饰的 static 字段不序列化」——说了两遍:static 本来就不参与序列化,transient 只对实例字段有意义,俩一起写属于画蛇添足。
- 「readObject 出来的对象是 new 出来的」——不走构造方法!JVM 直接按字节还原字段值。所以构造方法里的初始化逻辑不会执行(这也是它快的原因之一)。
- 「反序列化只需要处理 IOException」——
ClassNotFoundException是独立受检异常,不处理编译不过。9.1 三把钥匙里的forName同款。
自测一下
字节流(OutputStream 家族)。它搬运的是对象的字节序列,没有"文本翻译"环节;底层套 FileOutputStream 干实际搬运。
一个方法都没有。它是标记接口,作用是给 JVM 看的"准入证"——授权这个类的对象可以被序列化。没贴证的类 writeObject 直接 NotSerializableException。(4.3 接口知识点的特殊用法。)
不写:JVM 按类结构自动算版本号,类一改(加字段等)就和旧文件的号对不上,反序列化抛 InvalidClassException。写了固定值(1L):类小改动也能正常读回旧文件——兼容。
age = 0(数值型默认值),name = null(引用型默认值)。transient 字段不参与打包,拆包时按成员变量默认值补齐。
类定义:implements Serializable + private static final long serialVersionUID = 1L; + password 加 transient。写:new ObjectOutputStream(new FileOutputStream(...)).writeObject(stu);读:强转 (Student) ois.readObject(),password 读回来是 null(属于正常现象,需要时可重新录入)。
下一步学什么
- ☑ 10.5 序列化和反序列化 ✅ —— 点此复习:第十章 I-O/10.5 序列化和反序列化
- ☑ 10.6 本章小结 ✅ —— 点此复习:第十章 I-O/10.6 本章小结
相关笔记
- 第十章 I-O/10.2 字节流:方向口诀与关流姿势的地基
- 第十章 I-O/10.4 转换流:同为"套娃流",一个管翻译一个管打包
- 第四章 面向对象(下)/4.3 抽象类和接口:标记接口=接口的特殊玩法,收网
- 第九章 反射机制和注解/9.1 反射概述:ClassNotFoundException 同根同源——按名字找类
- 第四章 面向对象(下)/4.4 多态:readObject 强转,向下转型的老规矩