# 2026-08-08 对象序列化与反序列化 + 转换流练习(课堂练习用) > **说明**:练习今日新学知识点——**对象序列化与反序列化(对象持久化)**(`Serializable` 标记接口、`ObjectOutputStream.writeObject()` 序列化 / `ObjectInputStream.readObject()` 反序列化、`serialVersionUID` 版本兼容与 `InvalidClassException`、`transient` 跳过序列化、对象强转)以及**转换流**(`InputStreamReader` / `OutputStreamWriter`:字节 ↔ 字符的桥梁、字符集编码 / 解码、GBK / UTF-8 转码、乱码原理)。 --- ## 练习一:多个学生对象序列化与反序列化(对象集合持久化) **难度**:⭐⭐⭐⭐ **知识点**:`Serializable`、`ObjectOutputStream`、`ObjectInputStream`、`ArrayList` 集合、`writeObject()` / `readObject()`、对象强转、`FileOutputStream` / `FileInputStream` **场景描述**:课堂学了**对象持久化**:把内存中的对象用 `ObjectOutputStream` 序列化到磁盘文件,再用 `ObjectInputStream` 反序列化读回内存。但是**一个对象一次写入太麻烦**——本例把**多个 Student 对象装进 ArrayList 集合**,把"整个集合对象"一次性序列化到文件,再一次性反序列化读回。集合在 Java 中也是对象,同样可以序列化。 ### 案例需求 创建多个学生类对象写到文件中,再次读取到内存中。 ### 实现步骤 1. 创建序列化流对象 2. 创建多个学生对象 3. 将学生对象添加到集合中 4. 将集合对象序列化到文件中 5. 创建反序列化流对象 6. 将文件中的对象数据,读取到内存中 ### 代码框架 ```java import java.io.*; import java.util.ArrayList; // 学生类:想要被序列化,必须实现 Serializable 接口 class Student implements Serializable { private static final long serialVersionUID = 42L; // 固定序列化版本号 private Integer id; private String name; private Integer age; public Student() { } public Student(Integer id, String name, Integer age) { this.id = id; this.name = name; this.age = age; } // getter / setter 省略... @Override public String toString() { return "Student{id=" + id + ", name='" + name + "', age=" + age + "}"; } } public class ObjectListTest { public static void main(String[] args) throws IOException, ClassNotFoundException { // ========== 1、创建序列化流对象 ========== // TODO 1: new ObjectOutputStream(new FileOutputStream("D:/students.txt")) // ========== 2、创建多个学生对象 ========== // TODO 2: 创建张三、李四、王五 等 3 个 Student 对象 // ========== 3、将学生对象添加到集合中 ========== // TODO 3: new ArrayList(),用 list.add(...) 把 3 个学生加入集合 // ========== 4、将集合对象序列化到文件中 ========== // TODO 4: oos.writeObject(list); 一次把整个集合写入文件 // TODO 5: oos.close(); // ========== 5、创建反序列化流对象 ========== // TODO 6: new ObjectInputStream(new FileInputStream("D:/students.txt")) // ========== 6、将文件中的对象数据,读取到内存中 ========== // TODO 7: Object o = ois.readObject(); 读回的是 Object // TODO 8: 强转为 ArrayList 类型,遍历输出每个学生 // TODO 9: ois.close(); } } ``` **输出示例**: ``` 反序列化读取到的学生: Student{id=1001, name='张三', age=18} Student{id=1002, name='李四', age=19} Student{id=1003, name='王五', age=20} ``` **选做加分**: - 给 Student 的 `id` 加 `transient` 修饰再次序列化 / 反序列化,观察读回后 id 是否变成 null - 连续两次 `writeObject()` 序列化两个集合,再连续两次 `readObject()` 读回,体会**写入顺序与读取顺序必须一致** - 先写入集合再写入一个单独对象,反序列化时按相同顺序分别读取 **思考题**: 1. 为什么集合 `ArrayList` 也能被序列化?(提示:ArrayList 实现了 Serializable 接口,集合也是对象) 2. `readObject()` 返回什么类型?为什么需要强转?(提示:返回 Object,强转后才有 Student 的 toString / getter 可用) 3. 反序列化时为什么 main 要 `throws ClassNotFoundException`?(提示:readObject 需要根据文件里的类信息加载类,找不到就抛异常) 4. 序列化到文件的内容是什么?直接打开文件看是乱码吗?(提示:是二进制字节序列,不是文本) --- ## 练习二:transient 与 serialVersionUID 验证(类版本兼容实验) **难度**:⭐⭐⭐⭐ **知识点**:`transient` 关键字、`serialVersionUID`、`InvalidClassException`、序列化版本兼容 **场景描述**:课堂讲了序列化的两个"坑":① `transient` 修饰的属性**不参与序列化**;② 类结构(属性)变化后反序列化会抛 **`InvalidClassException`**,加上 `serialVersionUID` 固定版本号即可兼容。本例用实验把这两个知识点验证一遍。 ### 案例需求 验证 transient 属性能否被序列化,以及 serialVersionUID 对类版本变化的影响。 ### 实现步骤 1. 序列化一个带 transient 属性的学生对象到文件 2. 反序列化读回,观察 transient 属性的值是否为默认值(null) 3. 修改 Student 类的属性(新增 / 删除字段),去掉 serialVersionUID 再反序列化 → 观察是否报 InvalidClassException 4. 加上 serialVersionUID 固定版本号,再反序列化 → 验证兼容 ### 代码框架 ```java import java.io.*; // 学生类:id 用 transient 修饰,不参与序列化 class Student implements Serializable { // TODO: 练习时先去掉下面这行,修改类结构后观察 InvalidClassException private static final long serialVersionUID = 42L; private transient Integer id; // transient:序列化时跳过该属性 private String name; private Integer age; // TODO: 尝试新增一个属性(如 private String sex;)观察反序列化结果 public Student(Integer id, String name, Integer age) { this.id = id; this.name = name; this.age = age; } @Override public String toString() { return "Student{id=" + id + ", name='" + name + "', age=" + age + "}"; } } public class TransientTest { public static void main(String[] args) throws IOException, ClassNotFoundException { // ========== 1、序列化一个学生对象 ========== // TODO 1: 创建 Student 对象(id=1001, name=张三, age=18) // TODO 2: ObjectOutputStream 序列化到 D:/student.txt // ========== 2、反序列化读回,观察 transient 属性 ========== // TODO 3: ObjectInputStream 读回,强转后打印 // 预期:id=null(transient 不参与序列化,反序列化为默认值) } } ``` **输出示例**: ``` 反序列化结果:Student{id=null, name='张三', age=18} ← id 是 transient,变成 null ``` **选做加分**: - 修改 Student 类**新增**一个属性(如 `sex`),**去掉** serialVersionUID 后反序列化 → 观察 `InvalidClassException` 的异常信息 - 再加上 `serialVersionUID = 42L` 固定版本号,再次反序列化 → 观察不再报错,新属性 sex 值为 null - 用 `s instanceof Serializable` 判断一个对象能否被序列化 **思考题**: 1. `transient` 修饰的属性序列化后变成什么?(提示:反序列化后为默认值——引用类型 null、基本类型 0/false) 2. 去掉 serialVersionUID 后修改类结构,为什么反序列化会报 `InvalidClassException`?(提示:没有显式版本号时 JVM 自动生成一个,类结构变化 → 自动版本号变化 → 两端不一致) 3. 加上 serialVersionUID 后类结构变了为什么还能读?(提示:版本号一致即可兼容;有的属性正常赋值,没有的属性给默认值) 4. 哪些成员天然不参与序列化?(提示:static 修饰的类成员不属于对象,本身就不参与序列化) --- ## 练习三:转换流字符集转码(UTF-8 文件 → GBK 文件) **难度**:⭐⭐⭐ **知识点**:`InputStreamReader`、`OutputStreamWriter`、字符集、编码 / 解码、GBK / UTF-8、乱码原理 **场景描述**:课堂学了**转换流**:`InputStreamReader` 是字节流 → 字符流的桥梁(按指定字符集**解码**),`OutputStreamWriter` 是字符流 → 字节流的桥梁(按指定字符集**编码**)。现在把 `D:/abc.txt`(UTF-8 编码)转码为 `D:/hello.txt`(GBK 编码),体会"读端指定源字符集、写端指定目标字符集"的转码流程。 ### 案例需求 使用 UTF-8 字符集打开 abc.txt 文件,并将内容转换为 GBK 字符集写入到 hello.txt 文件。 ### 实现步骤 1. 创建输入转换流:`InputStreamReader` 包装文件字节输入流,指定 UTF-8 2. 创建输出转换流:`OutputStreamWriter` 包装文件字节输出流,指定 GBK 3. 用 char[1024] 循环读取字符并写入 4. 关闭流,释放资源 ### 代码框架 ```java import java.io.*; public class ConvertStreamTest { public static void main(String[] args) throws IOException { // ========== 1、输入转换流:按 UTF-8 解码 ========== // TODO 1: new InputStreamReader(new FileInputStream("D:/abc.txt"), "UTF-8") // ========== 2、输出转换流:按 GBK 编码 ========== // TODO 2: new OutputStreamWriter(new FileOutputStream("D:/hello.txt"), "GBK") // ========== 3、char[1024] 循环读取字符并写入 ========== // TODO 3: char[] chars = new char[1024]; // while ((count = isr.read(chars)) != -1) { osw.write(chars, 0, count); } // ========== 4、关闭流,释放资源 ========== // TODO 4: osw.close(); isr.close(); System.out.println("转码完成!UTF-8 的 abc.txt 已转为 GBK 的 hello.txt"); } } ``` **输出示例**: ``` 转码完成!UTF-8 的 abc.txt 已转为 GBK 的 hello.txt ``` (用 GBK 编码的记事本打开 `D:/hello.txt`,中文正常显示;文件大小比 UTF-8 文件小——一个汉字 GBK 占 2 字节) **选做加分**: - 把写端字符集改成 UTF-8(读写都是 UTF-8),对比文件字节大小差异,理解"同一中文在不同字符集中占字节数不同" - 反向转码:读取 GBK 文件 → 写入 UTF-8 文件,体会"读端指定 GBK、写端指定 UTF-8" - 用默认字符集的 `FileReader` 读取转码后的 GBK 文件,观察乱码现象;再改用指定 GBK 的转换流读取,验证乱码消失 **思考题**: 1. `InputStreamReader` 和 `FileReader` 有什么区别?(提示:FileReader 只能用平台默认字符集,InputStreamReader 可以手动指定字符集) 2. 转换流为什么属于"扩展流(处理流)"?(提示:它不直接操作文件,而是包装底层 FileInputStream / FileOutputStream 增加转码能力) 3. 如果读端写 UTF-8、写端也写 UTF-8,会发生什么?(提示:相当于按 UTF-8 拷贝文件内容) 4. 转码后 GBK 文件的汉字为什么比 UTF-8 的占字节少?(提示:GBK 汉字 2 字节,UTF-8 汉字 3 字节) --- # 参考答案要点 ## 练习一:ObjectListTest(多个学生对象集合序列化) ```java // 1、创建序列化流对象(TODO 1) ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/students.txt")); // 2、创建多个学生对象(TODO 2) Student s1 = new Student(1001, "张三", 18); Student s2 = new Student(1002, "李四", 19); Student s3 = new Student(1003, "王五", 20); // 3、将学生对象添加到集合中(TODO 3) ArrayList list = new ArrayList<>(); list.add(s1); list.add(s2); list.add(s3); // 4、将集合对象序列化到文件中(TODO 4~5) oos.writeObject(list); // 一次写入整个集合(集合也是对象) oos.close(); // 5、创建反序列化流对象(TODO 6) ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/students.txt")); // 6、将文件中的对象数据,读取到内存中(TODO 7~9) Object o = ois.readObject(); // 读回的是 Object ArrayList readList = (ArrayList) o; // 强转为 ArrayList for (Student stu : readList) { System.out.println(stu); // 自动调用重写的 toString() } ois.close(); ``` ```java // 选做:连续两次序列化 / 反序列化(写入顺序与读取顺序必须一致) oos.writeObject(list1); // 先写集合1 oos.writeObject(list2); // 再写集合2 Object o1 = ois.readObject(); // 先读集合1 Object o2 = ois.readObject(); // 再读集合2 ``` ## 练习二:TransientTest(transient + serialVersionUID 验证) ```java // 1、序列化一个学生对象(TODO 1~2) Student s = new Student(1001, "张三", 18); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/student.txt")); oos.writeObject(s); oos.close(); // 2、反序列化读回,观察 transient 属性(TODO 3) ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/student.txt")); Student readStu = (Student) ois.readObject(); System.out.println(readStu); // Student{id=null, name='张三', age=18} ← id 是 transient ois.close(); ``` ```java // 选做:serialVersionUID 版本兼容实验 // ① 去掉类中的 serialVersionUID,再往 Student 类新增属性 private String sex; // ② 反序列化 → 抛异常:java.io.InvalidClassException(JVM 自动版本号变化,两端不一致) // ③ 恢复 serialVersionUID = 42L,再次反序列化 → 不报错,sex 赋默认值 null ``` ## 练习三:ConvertStreamTest(UTF-8 → GBK 转码) ```java // 1、输入转换流:按 UTF-8 解码(TODO 1) InputStreamReader isr = new InputStreamReader(new FileInputStream("D:/abc.txt"), "UTF-8"); // 2、输出转换流:按 GBK 编码(TODO 2) OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:/hello.txt"), "GBK"); // 3、char[1024] 循环读取字符并写入(TODO 3) char[] chars = new char[1024]; int count; while ((count = isr.read(chars)) != -1) { osw.write(chars, 0, count); } // 4、关闭流,释放资源(TODO 4) osw.close(); isr.close(); System.out.println("转码完成!UTF-8 的 abc.txt 已转为 GBK 的 hello.txt"); ``` ```java // 选做:反向转码(GBK 文件 → UTF-8 文件) InputStreamReader isr = new InputStreamReader(new FileInputStream("D:/gbk.txt"), "GBK"); OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:/utf8.txt"), "UTF-8"); char[] chars = new char[1024]; int count; while ((count = isr.read(chars)) != -1) { osw.write(chars, 0, count); } osw.close(); isr.close(); ``` --- # 涵盖知识点总览 | 知识点 | 说明 | |--------|------| | `Serializable` 标记接口 | 无方法无属性,实现它即表示该类的对象可以被序列化 | | `ObjectOutputStream` | 对象序列化流,`writeObject(Object)` 把对象写入字节输出流 | | `ObjectInputStream` | 对象反序列化流,`readObject()` 从字节输入流读回对象(返回 Object 需强转) | | `writeObject(list)` | 集合也是对象(ArrayList 实现了 Serializable),可以一次性序列化整个集合 | | `readObject()` + 强转 | `(ArrayList) ois.readObject()` 强转后才能遍历访问 | | `serialVersionUID` | 固定类的版本号;类结构变化后反序列化不报 InvalidClassException,缺失属性赋默认值 | | `InvalidClassException` | 类结构变化且版本号不一致时,反序列化直接抛异常 | | `transient` | 修饰成员变量,序列化时跳过该属性,反序列化为默认值(引用类型 null) | | `toString()` 重写 | 打印对象 / 字符串拼接时自动调用,输出可读的对象内容 | | 序列化写读顺序 | 写入顺序与读取顺序必须一致(先写先读) | | `InputStreamReader` | 字节流 → 字符流桥梁(父类 Reader),按指定字符集**解码**字节为字符 | | `OutputStreamWriter` | 字符流 → 字节流桥梁(父类 Writer),按指定字符集**编码**字符为字节 | | 转换流构造方法 | `InputStreamReader(InputStream, String charsetName)` / `OutputStreamWriter(OutputStream, String charsetName)` | | 编码 / 解码 | 字符--编码-->字节--解码-->字符;A 编码 + B 解码 = 乱码 | | GBK / UTF-8 | 一个汉字:GBK 占 2 字节,UTF-8 占 3 字节 | | 转码流水线 | UTF-8 文件 → InputStreamReader(UTF-8) → char[] → OutputStreamWriter(GBK) → GBK 文件 | | 扩展流(处理流) | 转换流 / 对象流都包装底层字节流,不直接操作文件 |