前言
在日常开发中,我们经常会遇到这样的场景:
- 把内存里的对象保存到文件,下次启动再恢复;
- 把对象通过网络传输到另一个 JVM;
- 把对象缓存到 Redis、MQ 或磁盘。
这些场景背后都离不开一个基础能力 —— 对象序列化(Serialization)。
本文用一个最经典的 Employee 例子,把序列化 / 反序列化的用法、原理和注意事项一次讲清楚。内容适合刚接触 Java IO 的同学,也适合做面试前的快速回顾。
一、什么是序列化
Java 提供了一种对象序列化的机制:一个对象可以被表示为一个字节序列,这个字节序列里包含了:
- 对象的数据
- 对象的类型信息
- 对象中存储的数据类型
把序列化后的字节写入文件后,可以再从文件里读出来并反序列化:利用其中的类型信息和数据,在内存中重建出同样的对象。
最关键的一点:整个过程与 JVM 平台无关。在一台机器上序列化的对象,可以在完全不同的另一台机器上反序列化。
Java 里完成这件事的两个高层数据流类是:
ObjectOutputStream—— 负责序列化(写对象)ObjectInputStream—— 负责反序列化(读对象)
二、两个核心 API
ObjectOutputStream 里有很多 writeXxx 方法,但最特殊的一个是 writeObject,它会把一个对象序列化并发送到输出流:
public final void writeObject(Object obj) throws IOException
对应地,ObjectInputStream 里反序列化的方法是:
public final Object readObject()
throws IOException, ClassNotFoundException
注意两点:
readObject()的返回值是Object,拿到之后需要强制转换成实际类型。- 反序列化时,JVM 必须能在 classpath 中找到对应的类字节码,否则会抛出
ClassNotFoundException。
三、可序列化需要满足的条件
一个类的对象要想成功序列化,必须满足两个条件:
- 该类必须实现
java.io.Serializable接口; - 该类的所有属性都必须是可序列化的;如果某个属性不希望或不能被序列化,需要用
transient关键字标记。
想知道一个 JDK 标准类是否可序列化,直接看它是否实现了 Serializable 即可。
四、示例类:Employee
下面定义一个 Employee 类,它实现了 Serializable,其中 SSN 用 transient 标记,稍后会看到它在反序列化后的表现:
public class Employee implements java.io.Serializable {
public String name;
public String address;
public transient int SSN;
public int number;
public void mailCheck() {
System.out.println("Mailing a check to " + name + " " + address);
}
}
五、序列化对象:SerializeDemo
ObjectOutputStream 用来把对象写到输出流里。按 Java 的标准约定,序列化到文件时通常使用 .ser 后缀。
程序执行后会在 /tmp 下生成 employee.ser 文件。程序本身没有明显的控制台输出,可通过代码理解其作用:
import java.io.*;
public class SerializeDemo {
public static void main(String[] args) {
Employee e = new Employee();
e.name = "Reyan Ali";
e.address = "Phokka Kuan, Ambehta Peer";
e.SSN = 11122333;
e.number = 101;
try {
FileOutputStream fileOut = new FileOutputStream("/tmp/employee.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(e);
out.close();
fileOut.close();
System.out.printf("Serialized data is saved in /tmp/employee.ser");
} catch (IOException i) {
i.printStackTrace();
}
}
}
六、反序列化对象:DeserializeDemo
下面的程序从 /tmp/employee.ser 里读出之前序列化的 Employee 对象:
import java.io.*;
public class DeserializeDemo {
public static void main(String[] args) {
Employee e = null;
try {
FileInputStream fileIn = new FileInputStream("/tmp/employee.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
e = (Employee) in.readObject();
in.close();
fileIn.close();
} catch (IOException i) {
i.printStackTrace();
return;
} catch (ClassNotFoundException c) {
System.out.println("Employee class not found");
c.printStackTrace();
return;
}
System.out.println("Deserialized Employee...");
System.out.println("Name: " + e.name);
System.out.println("Address: " + e.address);
System.out.println("SSN: " + e.SSN);
System.out.println("Number: " + e.number);
}
}
运行结果:
Deserialized Employee...
Name: Reyan Ali
Address: Phokka Kuan, Ambehta Peer
SSN: 0
Number: 101
七、几个容易踩坑的点
结合上面的输出,有三点特别值得记住:
-
必须捕获
ClassNotFoundException
JVM 在反序列化时要找到对应的类字节码才能还原对象。一旦 classpath 中找不到Employee,就会抛出ClassNotFoundException。 -
readObject()返回的是Object
拿到之后需要手动强转成目标类型,例如:e = (Employee) in.readObject();。 -
transient字段不会被写入流
示例中SSN被序列化前是11122333,但由于声明为transient,它没有被写到输出流里。
因此反序列化之后,int类型的SSN只能拿到其默认值0(如果是对象类型会是null)。
总结
一张表回顾一下重点:
| 关注点 | 关键内容 |
|---|---|
|
核心能力 |
把对象 ↔ 字节序列互转,可跨 JVM / 跨平台 |
|
两个流 |
|
|
两个 API |
|
|
序列化条件 |
实现 |
|
跳过字段 |
用 |
|
文件约定 |
文件后缀一般用 |
|
异常处理 |
必须处理 |
|
反序列化返回值 |
|
序列化是 Java IO、RPC、分布式缓存、消息队列等诸多场景的地基。只要掌握好 Serializable、transient、writeObject / readObject 这几个关键点,后续再去学习 serialVersionUID、Externalizable、Kryo / Protobuf 等进阶内容,就会顺畅很多。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,后续会继续更新 Java 基础与后端工程化相关的内容。


4199

被折叠的 条评论
为什么被折叠?



