- xianaldai2026-08-2310677
本期是Java开发教程系列的第二十一期,本期介绍序列化:把内存里的对象整体保存成文件或传给网络,下次运行原样还原.第 12 期我们存的是文本文件,今天存的是"整个对象".若你已经熟悉
Serializable、ObjectOutputStream和serialVersionUID,可以跳过本期内容.*每期叠甲*:
时间仓促,水平有限,文中难免存在缺点和错误,恳请广大开发者和从业者指正.
一、序列化是什么:给对象拍张"照片"
第 11 期我们学会了把文本存进文件;第 20 期学会了存键值对配置.但存的对象呢?比如第 12 期那个
Student对象(有name、score两个字段),我们存的是"拼成字符串再读回来":```
writer.write(s.getName() + "," + s.getScore()); // 手动拼格式
// 读的时候再 split(",") 拆回来
```对象字段一多(十几个字段、还嵌套别的对象),手动拼串就活不下去了.有没有办法"把整个对象直接存进去,再原样恢复"?有! 序!列!化! :- 序列化:对象 → 字节流(可以存文件、传网络)
- 反序列化:字节流 → 新的对象(字段值原样还原)
打个比方:序列化是给对象拍一张"高清照片",反序列化是照着照片把对象重新搭出来.照片存在文件里,程序重启后还能"复活"它.
二、三步写出对象:Serializable + 两个流
第一步:让类实现
Serializable接口:它是个"标记接口",一个方法都没有,只用来告诉 JVM:“这个类的对象可以序列化”:```
import java.io.Serializable;public class Student implements Serializable {
public String name;
public double score;public Student(String name, double score) {
this.name = name;
this.score = score;
}
}
``**第二步:用ObjectOutputStream写出**:它是"对象专用"的输出流,和第 11 期的FileOutputStream一脉相承,只是包了一层:``
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;Student s = new Student("小明", 92.0);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("student.dat"))) {
out.writeObject(s); // 整个对象写出去
}
``**第三步:用ObjectInputStream读回**:``
import java.io.ObjectInputStream;
import java.io.FileInputStream;try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("student.dat"))) {
Student s = (Student) in.readObject(); // 读回来,强转成 Student
System.out.println(s.name + " " + s.score); // 小明 92.0
}
```注意三点:-
readObject()返回Object,要强转成目标类型- 读的字段顺序、类型和写的时候一致,对象才能还原
-
List<Student>同样可以直接序列化——集合对象本身就是对象,writeObject(list)一次全存,不用写循环一个对象的所有字段都会写进字节流,包括嵌套的对象(嵌套对象也会自动序列化,只要它的类也实现了
Serializable).这就是"整个对象"的含义.思考一下:我们前面说尽量少强转,这里怎么强转啦?
三、serialVersionUID:对象的"版本号"
上一节代码里我们没写版本号,能跑.但有个隐患:哪天
Student类加了一个字段(比如加了phone),再读以前存的文件:```
Exception in thread "main" java.io.InvalidClassException:
Student; local class incompatible:
stream classdesc serialVersionUID = 5838960950294633260,
local class serialVersionUID = -1234567890
```报错原因:序列化时,对象会带一个 serialVersionUID(序列化版本号)一起存进文件;反序列化时,JVM 拿这个号对比当前类的版本号,对不上就拒绝还原
因为类变了,旧数据还原出来可能驴唇不对马嘴.
如果代码里没写这个号,JVM 会根据类的结构自动算一个:结构一改,自动算出的号就变了,于是报错.这很烦:加一个字段,旧存档全废.
解决:在类里手动声明一个固定的版本号,并随类一起维护:```
public class Student implements Serializable {
private static final long serialVersionUID = 1L; // 固定版本号public String name;
public double score;
public String phone; // 新加的字段
}
``这样以后加字段、删字段,只要**更新这个号**(或保持兼容就不动它),JVM 就知道"这是同一版本的类",旧存档照样能读.规则一句话:**实现 Serializable 的类,都写上private static final long serialVersionUID`,加字段时手动决定是保持还是升级**.IDE 有快捷键自动生成,用上它.为什么是
static final?static 字段不参与序列化(它属于类),final 常量在编译期固定.它天生就是"给 JVM 看的元信息",不进字节流,反而永远可用.四、transient:有些字段不想存
对象里不是所有字段都该进文件:密码、临时缓存、依赖网络连接的对象……用
transient标记,序列化时自动跳过:```
public class Player implements Serializable {
private static final long serialVersionUID = 1L;public String name;
public transient String password; // 不落盘,不传网络
public transient Session session; // 运行时才有的东西,存了也没意义
}
```transient字段反序列化回来是默认值(null、0).规则:"该保存的"才让它存,"不该存的"用 transient 挡在门外.密码之类的敏感数据,绝对不能明文或者未经专门加密存在硬盘里.顺带说清一个易混点:
static字段也不参与序列化.但那是另一回事,static 字段属于类、不属于某个对象,序列化对象时自然不管它.transient是"对象里有,但我不想存";static是"根本不在对象里".五、安全提醒:反序列化不调用构造器
有一个细节值得现在记住,面试也常被问到:反序列化创建对象,不经过构造器:
readObject是 JVM 按字节流"拼"出对象,构造器、字段初始化的代码(比如private List<String> cache = new ArrayList<>())都不执行,字段直接按文件里的值填充.这带来两点后果:
- transient 字段回来是默认值.前面说的 null/0 就因此而来:构造器里的初始化没跑,存的时候又没存它
- 安全风险:如果把不可信的文件或网络数据拿去反序列化,攻击者可以精心构造字节流,让
readObject触发危险逻辑(构造特殊对象、执行恶意代码).这就是"反序列化攻击".所以规则是:只反序列化自己信任的数据(自己的存档),绝不对来路不明的输入调readObject(安全攻防阶段还会展开)第 12 期的存档是自己写的,放心用;但"从网上下载的存档文件直接读进内存"这种操作,就要警惕了.
六、小项目:学生列表存档
把第 12 期的学生列表整体存成
.dat,再读回,全程没有一行手动拼字符串:文件
Student.java:```
import java.io.Serializable;public class Student implements Serializable {
private static final long serialVersionUID = 1L;public String name;
public double score;public Student(String name, double score) {
this.name = name;
this.score = score;
}@Override
public String toString() {
return name + " " + score;
}
}
``文件Main.java:``
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;public class Main {
public static void main(String[] args) throws Exception {
// 造一个学生列表
List<Student> students = new ArrayList<>();
students.add(new Student("小明", 92));
students.add(new Student("小红", 58));
students.add(new Student("小刚", 76));// 整个列表序列化到文件
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("students.dat"))) {
out.writeObject(students);
}
System.out.println("已保存 " + students.size() + " 名学生到 students.dat");// 反序列化读回
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("students.dat"))) {
@SuppressWarnings("unchecked")
List<Student> loaded = (List<Student>) in.readObject();
System.out.println("读回 " + loaded.size() + " 名学生:");
for (Student s : loaded) {
System.out.println(s);
}
}
}
}
``运行结果:``
已保存 3 名学生到 students.dat
读回 3 名学生:
小明 92.0
小红 58.0
小刚 76.0
```动手实验(两步都值得做)^-^- 用记事本打开
students.dat,开头能看到AC ED两个字节,这是 Java 序列化文件的"魔数",就像文件的签名- 把
Student加一个字段(比如String phone),删掉serialVersionUID那行,再读旧存档.复现InvalidClassException;然后把serialVersionUID加回来,又能读了朝花夕拾:从"拼字符串"到"存整个对象"
第 12 期我们存存档,写的是
writer.write(s.getName() + "," + s.getScore());读回时split(",")拆。当时学的是"手动格式"。今天回头看,那套手动的本质就是自定义序列化格式,只是我们自己写的格式。今天学的ObjectOutputStream是 Java 官方的格式:通用、自动、支持嵌套对象,代价是文件不是人读的文本。两条路没有谁更好:要给人看的(配置文件、日志)用文本;要程序用的(存档、缓存)用对象序列化.第 12 期的学生成绩管理系统,今天升级成了"一键存档"版。总结
本期学会了把对象"整体搬走"再"原样搬回":
- 序列化:对象 ↔ 字节流,
Serializable标记接口 +ObjectOutputStream/ObjectInputStream- serialVersionUID:类的版本号,手动固定,类结构变化才可控;不写会自动生成,改类就报错
- transient:标记不想序列化的字段(密码、临时数据);static 本来就不参与
- 学生列表整体存档
.dat小项目,顺带认识AC ED魔数.下一期我们学习网络基础:程序第一次和别的程序(甚至另一台电脑)说话.
主楼 - 暂无回复