☕ 第十章 I/O
📄 一句话概括

序列化 = 把整个对象打包成字节序列写进文件(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
        }
    }
}
💡 30 秒实验一:撕掉准入证

把 implements Serializable 删掉再 writeObject → NotSerializableException: Student。异常名直接告诉你谁没资格。

💡 30 秒实验二:寄出后改类

先序列化一次(不写 serialVersionUID),然后给 Student 加个字段 double score,再运行反序列化那段 → InvalidClassException: local class incompatible。补上 serialVersionUID = 1L 固定值再测——加字段也能读回来。

💡 30 秒实验三:红牌字段读回来是啥

password 加 transient,序列化时设密码 "123456",反序列化后打印 → null。把类型换成 int 再试 → 0。

常见误区

  1. 「序列化后的文件是文本,能用记事本编辑」——不是,是字节序列。记事本打开是乱码,手改一个字节拆包就炸(版本核对/结构错乱)。
  2. 「实现了 Serializable 就万事大吉」——还得写 serialVersionUID,否则类一改旧文件全废。版本号是给自己留的后路。
  3. 「transient 修饰的 static 字段不序列化」——说了两遍:static 本来就不参与序列化,transient 只对实例字段有意义,俩一起写属于画蛇添足。
  4. 「readObject 出来的对象是 new 出来的」——不走构造方法!JVM 直接按字节还原字段值。所以构造方法里的初始化逻辑不会执行(这也是它快的原因之一)。
  5. 「反序列化只需要处理 IOException」——ClassNotFoundException 是独立受检异常,不处理编译不过。9.1 三把钥匙里的 forName 同款。

自测一下

❓ Q1:ObjectOutputStream 是字节流还是字符流?为什么?

字节流(OutputStream 家族)。它搬运的是对象的字节序列,没有"文本翻译"环节;底层套 FileOutputStream 干实际搬运。

❓ Q2:Serializable 接口里有哪些方法?它起什么作用?

一个方法都没有。它是标记接口,作用是给 JVM 看的"准入证"——授权这个类的对象可以被序列化。没贴证的类 writeObject 直接 NotSerializableException。(4.3 接口知识点的特殊用法。)

❓ Q3:serialVersionUID 不写会怎样?写了又怎样?

不写:JVM 按类结构自动算版本号,类一改(加字段等)就和旧文件的号对不上,反序列化抛 InvalidClassException。写了固定值(1L):类小改动也能正常读回旧文件——兼容。

❓ Q4:transient int age 和 transient String name,反序列化后各是什么值?

age = 0(数值型默认值),name = null(引用型默认值)。transient 字段不参与打包,拆包时按成员变量默认值补齐。

❓ Q5(毕业题):一个 Student 类有 name、age、password 三个字段。要求:①对象能存文件;②以后给类加新字段,旧文件还能读;③密码不能存进文件。写出类定义和读写核心代码。

类定义:implements Serializable + private static final long serialVersionUID = 1L; + password 加 transient。写:new ObjectOutputStream(new FileOutputStream(...)).writeObject(stu);读:强转 (Student) ois.readObject(),password 读回来是 null(属于正常现象,需要时可重新录入)。

下一步学什么

相关笔记