20260808-对象序列化与转换流练习.md 17 KB

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. 将文件中的对象数据,读取到内存中

代码框架

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<Student>(),用 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<Student> 类型,遍历输出每个学生
        // 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 固定版本号,再反序列化 → 验证兼容

代码框架

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. 关闭流,释放资源

代码框架

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(多个学生对象集合序列化)

// 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<Student> 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<Student> readList = (ArrayList<Student>) o; // 强转为 ArrayList<Student>
for (Student stu : readList) {
    System.out.println(stu);                          // 自动调用重写的 toString()
}
ois.close();
// 选做:连续两次序列化 / 反序列化(写入顺序与读取顺序必须一致)
oos.writeObject(list1);          // 先写集合1
oos.writeObject(list2);          // 再写集合2
Object o1 = ois.readObject();    // 先读集合1
Object o2 = ois.readObject();    // 再读集合2

练习二:TransientTest(transient + serialVersionUID 验证)

// 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();
// 选做:serialVersionUID 版本兼容实验
// ① 去掉类中的 serialVersionUID,再往 Student 类新增属性 private String sex;
// ② 反序列化 → 抛异常:java.io.InvalidClassException(JVM 自动版本号变化,两端不一致)
// ③ 恢复 serialVersionUID = 42L,再次反序列化 → 不报错,sex 赋默认值 null

练习三:ConvertStreamTest(UTF-8 → GBK 转码)

// 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");
// 选做:反向转码(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<Student>) 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 文件
扩展流(处理流) 转换流 / 对象流都包装底层字节流,不直接操作文件