第七章 输入输出流
第三节 序列化与反序列化
概述
序列化与反序列化是Java中进行对象持久化和数据传输的重要机制。通过序列化,可以将内存中的对象转换为字节流,方便存储或网络传输;而反序列化则是将字节流还原为Java对象。本节将全面介绍序列化与反序列化的概念、原理、实现方式以及相关注意事项,帮助考生深刻理解并掌握这一核心知识点,为全国计算机等级考试二级Java语言程序设计部分做好准备。
学习目标
- 理解序列化和反序列化的基本概念及应用场景
- 掌握Java中序列化与反序列化的实现方式和核心类
- 熟悉序列化机制的原理及注意事项
- 能够编写代码实现对象的序列化与反序列化
- 了解常见错误及其解决方案
- 掌握序列化在实际项目中的典型应用
核心概念
1. 序列化(Serialization)
序列化是指将Java对象转换成字节序列的过程。序列化后的对象可以被写入文件、传输到网络、存储到数据库等。该过程确保对象的状态能在不同环境中保持一致。
2. 反序列化(Deserialization)
反序列化是序列化的逆过程,将字节流转换为Java对象,使程序可以恢复之前保存的对象状态。
3. Serializable接口
这是Java中实现序列化的标记接口。只有实现了Serializable接口的类的对象才能被序列化。
4. transient关键字
用于修饰不需要序列化的成员变量,序列化时该变量不会被保存。
5. serialVersionUID
序列化版本号,用于验证序列化对象和反序列化类版本的一致性,防止版本不兼容导致错误。
原理分析
序列化机制基于Java的IO流体系,主要通过ObjectOutputStream和ObjectInputStream实现。
序列化流程:
- 对象必须实现Serializable接口。
- 创建ObjectOutputStream,调用writeObject()方法将对象写入输出流。
- JVM根据对象属性和类型信息,将对象转换成字节序列。
反序列化流程:
- 创建ObjectInputStream,调用readObject()方法读取字节流。
- JVM根据字节流中的信息重构对象实例。
- 如果serialVersionUID匹配,反序列化成功,否则抛出异常。
序列化的底层原理包含将对象的类信息、属性值以及引用关系编码到字节流中,保证对象的完整性和关联性。
详细内容
1. 实现序列化的步骤
实现序列化主要涉及以下几个方面:
实现Serializable接口:
- 这是一个标记接口,不包含任何方法,仅用来标识该类支持序列化。
定义serialVersionUID:
- 建议显式声明serialVersionUID,避免默认计算带来的兼容性问题。
- 格式示例:
private static final long serialVersionUID = 1L;
使用ObjectOutputStream写对象:
- 创建文件输出流(FileOutputStream),再包装ObjectOutputStream。
- 调用writeObject()方法写入对象。
使用ObjectInputStream读对象:
- 创建文件输入流(FileInputStream),再包装ObjectInputStream。
- 调用readObject()方法读取对象。
2. transient关键字的作用
- 用于修饰不想序列化的成员变量。
- 在序列化时,transient变量不会被保存,反序列化后该变量为默认值。
- 适用于保存敏感信息或非持久化数据。
3. 序列化的版本控制
- serialVersionUID用于版本控制。
- 如果序列化的类和反序列化的类serialVersionUID不匹配,程序会抛出InvalidClassException。
- 建议在类定义时显式声明serialVersionUID,防止自动生成的版本号因代码变动而改变。
4. 自定义序列化
- 可以通过实现private void writeObject(ObjectOutputStream oos)和private void readObject(ObjectInputStream ois)来定制序列化和反序列化过程。
- 适合需要对某些字段进行特殊处理(如加密、压缩等)时使用。
5. 对象的深克隆实现
- 利用序列化和反序列化可以实现对象的深克隆。
- 通过将对象序列化为字节流,再反序列化为新对象,达到深复制的效果。
典型实例分析
实例一:简单序列化与反序列化
背景:实现一个学生Student类的序列化和反序列化。
import java.io.*;
class Student implements Serializable {
private static final long serialVersionUID = 1L;
String name;
int age;
transient String password; // 不序列化密码
public Student(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + ", password='" + password + "'}";
}
}
public class SerializeDemo {
public static void main(String[] args) {
Student s = new Student("张三", 20, "123456");
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.dat"))) {
oos.writeObject(s);
System.out.println("序列化成功");
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.dat"))) {
Student s2 = (Student) ois.readObject();
System.out.println("反序列化结果:" + s2);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
分析:
- password字段被transient修饰,序列化后不保存,反序列化时为null。
- 通过文件保存和读取实现对象的持久化。
结论:掌握transient关键字和基本的序列化反序列化流程。
实例二:序列化版本控制
背景:演示serialVersionUID的作用。
步骤:
- 定义类Person,声明serialVersionUID = 1L,序列化后保存到文件。
- 修改Person类,添加新字段,保持serialVersionUID不变。
- 反序列化旧文件,观察是否成功。
代码示例(简要)
class Person implements Serializable {
private static final long serialVersionUID = 1L;
String name;
int age;
// 新增字段
String address;
}
分析:
- serialVersionUID一致,反序列化成功,新增字段address默认null。
- 如果serialVersionUID不一致,则抛异常。
结论:显式声明serialVersionUID,保证版本兼容性。
实例三:自定义序列化
背景:对用户密码进行加密存储。
class User implements Serializable {
private static final long serialVersionUID = 1L;
String username;
transient String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
String encrypted = encrypt(password);
oos.writeObject(encrypted);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
String encrypted = (String) ois.readObject();
password = decrypt(encrypted);
}
private String encrypt(String pwd) {
// 简单示例,实际应更复杂
return new StringBuilder(pwd).reverse().toString();
}
private String decrypt(String encryptedPwd) {
return new StringBuilder(encryptedPwd).reverse().toString();
}
}
分析:
- password定义为transient,避免默认序列化。
- 重写writeObject和readObject实现加密和解密。
结论:自定义序列化满足特殊需求,增强安全性。
常见误区与注意事项
未实现Serializable接口就进行序列化
- 错误:程序运行时抛出NotSerializableException。
- 正确:必须实现Serializable接口。
忽略serialVersionUID的声明
- 错误:类结构变更时,默认serialVersionUID变化,导致反序列化失败。
- 正确:手动声明serialVersionUID,保证版本兼容。
错误使用transient关键字
- 错误:敏感信息未用transient修饰,导致泄露。
- 正确:敏感字段用transient修饰或自定义序列化处理。
忽略对象中引用类型的序列化
- 错误:对象中包含未序列化的引用对象,导致序列化失败。
- 正确:引用对象类也必须实现Serializable。
反序列化时强制类型转换错误
- 错误:反序列化对象后未正确强转,导致ClassCastException。
- 正确:确保类型匹配。
应用场景
- 持久化存储:将对象状态保存到文件或数据库,便于程序重启后恢复数据。
- 网络传输:通过序列化将对象转换为字节流,发送至远程主机,实现远程方法调用(RMI)等。
- 缓存机制:将复杂对象序列化存入缓存,提升访问效率。
- 深克隆:利用序列化和反序列化实现对象的深度复制。
- 配置管理:将应用配置对象序列化保存,方便加载和修改。
知识拓展
Externalizable接口
- 继承自Serializable,提供writeExternal和readExternal方法,自定义序列化流程。
- 相比Serializable,更灵活控制序列化细节。
Java序列化的安全问题
- 不安全的反序列化可能导致远程代码执行,应谨慎使用。
- 可以采用白名单校验、使用安全框架等方法防范风险。
JSON与XML序列化
- 除Java原生序列化外,常用JSON、XML格式进行数据交换。
- 更加跨语言、可读,适合Web应用。
序列化性能优化
- 使用transient减少序列化数据量。
- 使用第三方高性能序列化库,如Kryo、Protobuf。
总结回顾
- 序列化是将对象转换为字节流的过程,反序列化是字节流还原为对象。
- 序列化必须实现Serializable接口,且推荐显式声明serialVersionUID。
- 通过transient关键字控制字段是否参与序列化。
- 可通过重写writeObject和readObject实现自定义序列化。
- 常见错误包括未实现Serializable、忽略serialVersionUID、未处理引用对象等。
- 序列化广泛应用于持久化、网络传输、缓存等场景。
- 理解并掌握序列化机制,有助于写出安全、高效且兼容性良好的Java程序。
掌握本节内容是全国计算机等级考试二级Java语言程序设计的重要知识点,建议考生反复练习代码,实现理论与实践结合,提升综合能力。
参考资料
- 《Java核心技术 卷I》—— Cay S. Horstmann
- 《Java编程思想》—— Bruce Eckel
- Oracle官方Java文档 - Serialization