说明:练习今日新学知识点——对象序列化与反序列化(对象持久化)(
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 中也是对象,同样可以序列化。
创建多个学生类对象写到文件中,再次读取到内存中。
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}
选做加分:
id 加 transient 修饰再次序列化 / 反序列化,观察读回后 id 是否变成 nullwriteObject() 序列化两个集合,再连续两次 readObject() 读回,体会写入顺序与读取顺序必须一致思考题:
ArrayList 也能被序列化?(提示:ArrayList 实现了 Serializable 接口,集合也是对象)readObject() 返回什么类型?为什么需要强转?(提示:返回 Object,强转后才有 Student 的 toString / getter 可用)throws ClassNotFoundException?(提示:readObject 需要根据文件里的类信息加载类,找不到就抛异常)难度:⭐⭐⭐⭐
知识点:transient 关键字、serialVersionUID、InvalidClassException、序列化版本兼容
场景描述:课堂讲了序列化的两个"坑":① transient 修饰的属性不参与序列化;② 类结构(属性)变化后反序列化会抛 InvalidClassException,加上 serialVersionUID 固定版本号即可兼容。本例用实验把这两个知识点验证一遍。
验证 transient 属性能否被序列化,以及 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
选做加分:
sex),去掉 serialVersionUID 后反序列化 → 观察 InvalidClassException 的异常信息serialVersionUID = 42L 固定版本号,再次反序列化 → 观察不再报错,新属性 sex 值为 nulls instanceof Serializable 判断一个对象能否被序列化思考题:
transient 修饰的属性序列化后变成什么?(提示:反序列化后为默认值——引用类型 null、基本类型 0/false)InvalidClassException?(提示:没有显式版本号时 JVM 自动生成一个,类结构变化 → 自动版本号变化 → 两端不一致)难度:⭐⭐⭐
知识点:InputStreamReader、OutputStreamWriter、字符集、编码 / 解码、GBK / UTF-8、乱码原理
场景描述:课堂学了转换流:InputStreamReader 是字节流 → 字符流的桥梁(按指定字符集解码),OutputStreamWriter 是字符流 → 字节流的桥梁(按指定字符集编码)。现在把 D:/abc.txt(UTF-8 编码)转码为 D:/hello.txt(GBK 编码),体会"读端指定源字符集、写端指定目标字符集"的转码流程。
使用 UTF-8 字符集打开 abc.txt 文件,并将内容转换为 GBK 字符集写入到 hello.txt 文件。
InputStreamReader 包装文件字节输入流,指定 UTF-8OutputStreamWriter 包装文件字节输出流,指定 GBKimport 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 字节)
选做加分:
FileReader 读取转码后的 GBK 文件,观察乱码现象;再改用指定 GBK 的转换流读取,验证乱码消失思考题:
InputStreamReader 和 FileReader 有什么区别?(提示:FileReader 只能用平台默认字符集,InputStreamReader 可以手动指定字符集)// 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
// 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
// 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 文件 |
| 扩展流(处理流) | 转换流 / 对象流都包装底层字节流,不直接操作文件 |