目录
[1.1 序列化到底是什么?](#1.1 序列化到底是什么?)
[1.2 Serializable接口的作用?](#1.2 Serializable接口的作用?)
案例2.1:消息队列传递自定义对象(ActiveMQ/RabbitMQ)
场景3:对象需要存入HttpSession(Web开发必记)
[1. 必须显式指定serialVersionUID](#1. 必须显式指定serialVersionUID)
[2. 敏感字段用transient修饰](#2. 敏感字段用transient修饰)
[3. 嵌套对象需逐级实现序列化](#3. 嵌套对象需逐级实现序列化)
[4. 自定义序列化逻辑(可选)](#4. 自定义序列化逻辑(可选))
异常1:NotSerializableException(最常见)
前言:作为Java程序员,我们几乎每天都会和实体类打交道,而
Serializable接口更是高频出现------很多人习惯性地给实体类加上implements Serializable,却不清楚"为什么加""什么时候必须加""什么时候加了多余"。甚至遇到NotSerializableException异常时,只能瞎猜乱改。本文结合Java实战场景,拆解序列化的核心逻辑,明确哪些场景必须实现序列化,附代码案例和避坑技巧,适配日常开发、面试备考,纯干货无废话!
一、先铺垫:3分钟搞懂Java序列化核心(避免踩基础坑)
在聊场景之前,先明确2个核心问题,避免后续理解偏差------毕竟很多人踩坑,都是因为没搞懂序列化的本质。
1.1 序列化到底是什么?
简单说:序列化就是将Java对象的状态(成员变量值)转换成字节序列,反序列化则是将字节序列恢复成原本的Java对象。
核心目的有两个:
|----------------------------------|
| ① 实现对象的持久化(比如存到文件、数据库) |
| ② 实现对象的跨进程/跨网络传输(比如RPC调用、消息队列传递) |
1.2 Serializable接口的作用?
Java中的Serializable是一个标记接口(Marker Interface) ------没有任何抽象方法,仅用于"告诉JVM":这个类的对象可以被序列化机制处理。
注意3个关键细节(实战必记):
序列化只保存对象的非静态成员变量(静态变量属于类,不属于对象,不参与序列化);
被
transient修饰的成员变量,不参与序列化(后续实战会用到);必须显式指定serialVersionUID(否则类结构微调后,反序列化会报
InvalidClassException)。
示例(基础序列化类写法):
java
// 标准写法:实现Serializable + 显式指定serialVersionUID
public class User implements Serializable {
// 序列化版本号,建议从1开始,后续类结构变化可修改
private static final long serialVersionUID = 1L;
private Long id;
private String username;
// 敏感字段,不参与序列化(比如密码)
private transient String password;
// 构造器、getter/setter省略
}
二、重点:4种必须实现序列化的核心场景(实战高频)
以下场景是开发中最常见、且必须 实现Serializable的情况,缺少则直接抛出异常,结合代码案例拆解,一看就懂。
场景1:对象需要持久化到磁盘/文件(最基础场景)
当你需要将Java对象保存到本地文件、磁盘,或者存入数据库的BLOB字段(比如保存对象快照)时,必须实现序列化------因为JVM需要通过序列化将对象转换成字节流,才能写入存储介质。
实战案例:将用户对象保存到本地文件,后续读取恢复
错误示例(未实现序列化):
java
// 未实现Serializable,序列化时直接抛NotSerializableException
public class User {
private Long id;
private String username;
// 构造器、getter/setter省略
}
// 测试序列化
public class SerializeTest {
public static void main(String[] args) {
User user = new User(1L, "zhangsan");
// 尝试将对象写入文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
oos.writeObject(user); // 此处直接抛出NotSerializableException
} catch (IOException e) {
e.printStackTrace();
}
}
}
正确示例(实现序列化):
java
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
// 构造器、getter/setter省略
}
// 完整序列化+反序列化
public class SerializeTest {
public static void main(String[] args) {
// 1. 序列化对象到文件
User user = new User(1L, "zhangsan");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
oos.writeObject(user);
System.out.println("对象序列化成功,已写入文件");
} catch (IOException e) {
e.printStackTrace();
}
// 2. 反序列化:从文件恢复对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
User deserializedUser = (User) ois.readObject();
System.out.println("反序列化成功,用户名:" + deserializedUser.getUsername());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
补充:日常开发中,"对象持久化"除了本地文件,还包括缓存持久化(比如本地缓存Caffeine保存对象时,若开启持久化,对象需序列化)。
场景2:对象需要跨进程/跨网络传输(分布式开发必用)
分布式系统中,对象需要跨节点、跨进程传输(比如RPC调用、消息队列传递)时,必须实现序列化------因为网络传输的是字节流,而非对象本身,需要通过序列化将对象转换成字节流,传输完成后再反序列化成对象。
这是Java程序员最常遇到的场景,重点看2个实战案例。
案例2.1:消息队列传递自定义对象(ActiveMQ/RabbitMQ)
当使用JMS(ActiveMQ)、RabbitMQ等消息中间件,传递自定义对象作为消息体时,该对象必须实现序列化。
java
// 消息体对象:必须实现Serializable
public class OrderMessage implements Serializable {
private static final long serialVersionUID = 2L;
private String orderId;
private BigDecimal amount;
private Date createTime;
// 构造器、getter/setter省略
}
// 发送消息(ActiveMQ示例)
public class MessageSender {
public void sendOrderMessage(OrderMessage message) {
// 1. 获取MQ连接(实际开发中可封装成工具类)
Connection connection = getMQConnection();
try {
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue("order.queue");
MessageProducer producer = session.createProducer(queue);
// 2. 将自定义对象包装成ObjectMessage
ObjectMessage objectMessage = session.createObjectMessage(message);
producer.send(objectMessage); // 若OrderMessage未序列化,此处抛异常
// 3. 关闭资源
producer.close();
session.close();
} catch (JMSException e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
}
案例2.2:RPC远程方法调用(Dubbo/RMI)
无论是Java原生的RMI,还是主流的Dubbo,远程调用的"参数对象"和"返回值对象",都必须实现序列化。
java
// 1. 远程服务接口(Dubbo示例)
public interface UserService {
// 返回值User必须实现Serializable
User getUserById(Long id);
// 参数UserParam必须实现Serializable
Boolean updateUser(UserParam userParam);
}
// 2. 自定义参数对象(必须序列化)
public class UserParam implements Serializable {
private static final long serialVersionUID = 3L;
private Long id;
private String username;
private String phone;
// 构造器、getter/setter省略
}
// 3. 服务实现类(无需额外处理序列化,只需保证参数/返回值序列化)
@Service
public class UserServiceImpl implements UserService {
@Override
public User getUserById(Long id) {
return new User(id, "lisi", "13900139000");
}
@Override
public Boolean updateUser(UserParam userParam) {
// 业务逻辑省略
return true;
}
}
场景3:对象需要存入HttpSession(Web开发必记)
Web开发中,我们经常会将用户信息、会话信息存入HttpSession中。而Tomcat、Jetty等Web容器,会对Session进行"钝化/活化"处理(比如容器重启、内存不足时,将Session持久化到磁盘,后续恢复)。
此时,Session中存储的所有对象,必须实现序列化------否则容器钝化Session时,会抛出NotSerializableException。
实战案例:Session中存储登录用户信息
java
// Web Servlet示例(SpringMVC同理)
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 接收请求参数
String username = req.getParameter("username");
String password = req.getParameter("password");
// 2. 校验用户(业务逻辑省略)
User loginUser = new User(1L, username, password);
// 3. 存入Session(User必须实现Serializable)
HttpSession session = req.getSession();
session.setAttribute("loginUser", loginUser); // 未序列化则抛异常
// 4. 跳转页面
resp.sendRedirect("/index");
}
}
补充:SpringBoot项目中,若使用@SessionAttributes注解存储对象,该对象也必须实现序列化。
场景4:自定义对象作为序列化集合的Key/元素
JDK中的很多集合类(比如HashMap、ArrayList、HashSet),本身已经实现了Serializable接口。但如果集合中存放的"自定义对象"未序列化,那么当序列化这个集合时,会直接失败。
尤其注意:自定义对象作为HashMap的Key时 ,除了实现Serializable,还必须重写hashCode()和equals()方法(否则反序列化后,Key无法匹配)。
实战案例:HashMap存储自定义对象(作为Key)并序列化
java
// 自定义Key对象(必须实现Serializable + 重写hashCode/equals)
public class UserKey implements Serializable {
private static final long serialVersionUID = 4L;
private Long userId;
// 重写hashCode和equals(关键)
@Override
public int hashCode() {
return Objects.hash(userId);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
UserKey userKey = (UserKey) obj;
return Objects.equals(userId, userKey.userId);
}
// 构造器、getter/setter省略
}
// 测试集合序列化
public class HashMapSerializeTest {
public static void main(String[] args) {
Map<UserKey, String> userMap = new HashMap<>();
userMap.put(new UserKey(1L), "zhangsan");
userMap.put(new UserKey(2L), "lisi");
// 序列化HashMap到文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("userMap.dat"))) {
oos.writeObject(userMap); // 若UserKey未序列化,此处抛异常
System.out.println("HashMap序列化成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}
三、避坑:不需要实现序列化的3种场景
很多程序员会"过度序列化"------只要是实体类,就加Serializable,其实完全没必要,以下场景无需实现:
仅在当前进程内存中使用的对象:比如方法内的局部变量、仅在当前类中使用的对象,不需要持久化、不需要跨网络传输,无需序列化;
使用第三方序列化框架(非JDK原生) :比如FastJSON、Jackson、Protobuf等,这些框架不依赖JVM原生序列化机制,无需实现
Serializable(比如SpringBoot接口返回JSON对象,无需序列化);静态内部类/不可变对象(无持久化/传输需求) :静态内部类不依赖外部类对象,若仅在当前进程使用,无需序列化;比如
String、Integer等包装类(已实现Serializable),但自定义不可变对象,若无需持久化/传输,无需额外实现。
四、实战最佳实践:序列化避坑技巧(必记)
掌握以下技巧,能避免90%的序列化相关异常,提升代码规范性。
1. 必须显式指定serialVersionUID
这是最容易被忽略、也最容易踩坑的点!若不显式指定,JVM会根据类的结构(字段、方法)自动生成一个 serialVersionUID,一旦类结构微调(比如新增一个非transient字段),自动生成的 serialVersionUID 会变化,导致反序列化失败(报InvalidClassException)。
规范写法:
java
// 显式指定,后续类结构变化时,手动调整版本号(比如从1L改成2L)
private static final long serialVersionUID = 1L;
2. 敏感字段用transient修饰
密码、令牌、身份证号等敏感信息,不需要参与序列化(避免持久化/传输时泄露),用transient修饰,修饰后该字段不会被序列化,反序列化后值为默认值(String为null,int为0)。
示例:
java
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
// 敏感字段,不序列化
private transient String password;
private transient String token;
}
3. 嵌套对象需逐级实现序列化
若一个类中包含"自定义嵌套对象"(比如User类包含Address对象),那么不仅当前类要实现序列化,嵌套对象也必须实现序列化------否则序列化当前类时,会抛出异常。
java
// 嵌套对象:必须实现Serializable
public class Address implements Serializable {
private static final long serialVersionUID = 5L;
private String province;
private String city;
}
// 主对象:实现Serializable,嵌套对象也已实现
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private Address address; // 嵌套对象,已序列化
}
4. 自定义序列化逻辑(可选)
若默认序列化逻辑不满足需求(比如敏感字段加密后再序列化),可重写writeObject()和readObject()方法(这两个方法是JVM序列化时的默认调用方法)。
java
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private transient String password; // 不参与默认序列化,但可自定义序列化
// 自定义序列化:加密密码后序列化
private void writeObject(ObjectOutputStream out) throws IOException {
// 1. 执行默认序列化(处理非transient字段)
out.defaultWriteObject();
// 2. 自定义处理password:加密后序列化
String encryptPwd = encrypt(password); // 自定义加密方法
out.writeObject(encryptPwd);
}
// 自定义反序列化:解密密码
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 1. 执行默认反序列化
in.defaultReadObject();
// 2. 解密密码并赋值
String encryptPwd = (String) in.readObject();
this.password = decrypt(encryptPwd); // 自定义解密方法
}
// 模拟加密方法(实际开发用真实加密算法)
private String encrypt(String pwd) {
return Base64.getEncoder().encodeToString(pwd.getBytes());
}
// 模拟解密方法
private String decrypt(String encryptPwd) {
return new String(Base64.getDecoder().decode(encryptPwd));
}
}
五、常见异常排查(实战必备)
开发中遇到序列化相关异常,按以下思路排查,高效解决问题。
异常1:NotSerializableException(最常见)
核心原因:对象本身、或其嵌套对象、或集合中的元素,未实现
Serializable;排查步骤:
|------------------------------------|
| ① 检查当前对象是否实现Serializable |
| ② 检查所有成员变量(包括嵌套对象)是否实现Serializable |
| ③ 若为集合,检查集合中的元素是否实现Serializable |
- 解决方法:给缺失的类添加
implements Serializable,显式指定serialVersionUID。
异常2:InvalidClassException
核心原因:序列化和反序列化时,类的
serialVersionUID不一致;或类结构发生不兼容变化(比如删除了已序列化的字段);排查步骤:
|---------------------------------------|
| ① 检查序列化和反序列化两端的类,serialVersionUID是否一致 |
| ② 检查类结构是否有不兼容修改 |
- 解决方法:统一两端的serialVersionUID;避免删除已序列化的字段(若必须修改,升级serialVersionUID,并做好兼容处理)。
六、总结(面试+实战双重点)
对于Java程序员来说,序列化不仅是日常开发必备技能,也是面试高频考点(比如"什么时候必须实现Serializable?""transient关键字的作用?")。
核心总结3句话,记牢不踩坑:
必须实现序列化的4种场景:对象持久化到磁盘、跨网络/跨进程传输、存入HttpSession、作为序列化集合的Key/元素;
序列化核心规范:显式指定serialVersionUID、敏感字段用transient、嵌套对象逐级序列化;
无需序列化:仅内存使用、用第三方序列化框架、无持久化/传输需求的静态内部类/不可变对象。
其实序列化本身不难,重点是"分清场景"------不盲目实现,不遗漏必须实现的情况,就能避免大部分问题。希望本文能帮你彻底搞懂序列化的使用边界,提升开发效率,避开面试坑!
最后,收藏本文,后续开发遇到序列化问题,直接对照排查;如果觉得有用,欢迎点赞+关注,持续分享Java实战干货~