学习目标
掌握序列化的基本原理、核心接口与主要应用场景
序列化核心概念
序列化是将对象转换为字节序列的过程,反序列化则是将字节序列恢复为对象的过程。
序列化 (Serialization)
Object → 字节序列 (byte stream)
反序列化 (Deserialization)
字节序列 (byte stream) → Object
序列化的主要应用场景
- 对象持久化:将对象保存到文件或数据库中
- 网络传输:在分布式系统中传输对象
- 深度复制:创建对象的完整副本
- 缓存机制:将对象状态缓存到内存或磁盘
- 远程方法调用:RMI、RPC等技术的底层支持
Java序列化核心接口
接口/类 |
描述 |
关键方法 |
Serializable |
标记接口,表示类可序列化 |
无方法 |
Externalizable |
提供自定义序列化控制 |
writeExternal(), readExternal() |
ObjectOutputStream |
将对象序列化为字节流 |
writeObject() |
ObjectInputStream |
将字节流反序列化为对象 |
readObject() |
重要概念
Serializable是一个标记接口,没有需要实现的方法。它的主要作用是告诉JVM该类可以被序列化。
学习目标
能够正确实现可序列化类并进行对象序列化与反序列化操作
实现可序列化类
import java.io.Serializable;
public class User implements Serializable {
// 序列化版本UID,用于版本控制
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient字段不会被序列化
private int age;
public User(String username, String password, int age) {
this.username = username;
this.password = password;
this.age = age;
}
// Getter和Setter方法
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", age=" + age +
'}';
}
}
序列化与反序列化操作
import java.io.*;
public class BasicSerializationDemo {
public static void main(String[] args) {
// 创建用户对象
User user = new User("john_doe", "secret123", 30);
// 序列化对象到文件
serializeObject(user, "user.ser");
// 从文件反序列化对象
User deserializedUser = deserializeObject("user.ser");
// 比较原始对象和反序列化后的对象
System.out.println("原始对象: " + user);
System.out.println("反序列化对象: " + deserializedUser);
}
/**
* 序列化对象到文件
* @param object 要序列化的对象
* @param filename 文件名
*/
private static void serializeObject(Object object, String filename) {
try (FileOutputStream fileOut = new FileOutputStream(filename);
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(object);
System.out.println("对象已序列化到: " + filename);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 从文件反序列化对象
* @param filename 文件名
* @return 反序列化的对象
*/
private static User deserializeObject(String filename) {
User object = null;
try (FileInputStream fileIn = new FileInputStream(filename);
ObjectInputStream in = new ObjectInputStream(fileIn)) {
object = (User) in.readObject();
System.out.println("对象已从 " + filename + " 反序列化");
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return object;
}
}
关键点说明
transient
关键字:标记不需要序列化的字段,如密码等敏感信息。
serialVersionUID
:显式声明版本ID,避免自动生成导致的兼容性问题。
学习目标
掌握writeObject()和readObject()方法实现自定义序列化逻辑
自定义序列化方法
import java.io.*;
public class CustomUser implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password;
private int age;
public CustomUser(String username, String password, int age) {
this.username = username;
this.password = password;
this.age = age;
}
// 自定义序列化逻辑
private void writeObject(ObjectOutputStream out) throws IOException {
// 调用默认序列化方法
out.defaultWriteObject();
// 对密码进行简单加密后序列化
String encrypted = encrypt(password);
out.writeObject(encrypted);
}
// 自定义反序列化逻辑
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
// 调用默认反序列化方法
in.defaultReadObject();
// 读取并解密密码
String encrypted = (String) in.readObject();
this.password = decrypt(encrypted);
}
// 简单加密方法
private String encrypt(String input) {
// 实际应用中应使用更安全的加密算法
return new StringBuilder(input).reverse().toString();
}
// 简单解密方法
private String decrypt(String input) {
return new StringBuilder(input).reverse().toString();
}
// Getter和Setter方法
// ...
@Override
public String toString() {
return "CustomUser{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", age=" + age +
'}';
}
}
使用场景
- 加密敏感字段
- 序列化非Serializable对象
- 优化序列化性能
- 序列化后执行额外操作
注意: 自定义序列化方法必须是private的,否则不会被JVM调用。
学习目标
掌握Externalizable接口实现完全自定义序列化
Externalizable vs Serializable
特性 |
Serializable |
Externalizable |
控制级别 |
部分控制(可选自定义方法) |
完全控制 |
默认行为 |
自动序列化所有非transient字段 |
无默认行为 |
性能 |
相对较低 |
较高(可优化) |
使用复杂度 |
简单 |
复杂 |
Externalizable实现示例
import java.io.*;
public class ExternalizableUser implements Externalizable {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private int age;
// 必须有无参构造函数
public ExternalizableUser() {
System.out.println("无参构造函数被调用");
}
public ExternalizableUser(String username, String password, int age) {
this.username = username;
this.password = password;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 完全控制序列化过程
out.writeObject(username);
out.writeObject(encrypt(password));
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException {
// 完全控制反序列化过程
username = (String) in.readObject();
password = decrypt((String) in.readObject());
age = in.readInt();
}
// 简单加密方法
private String encrypt(String input) {
return new StringBuilder(input).reverse().toString();
}
// 简单解密方法
private String decrypt(String input) {
return new StringBuilder(input).reverse().toString();
}
// Getter和Setter方法
// ...
@Override
public String toString() {
return "ExternalizableUser{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", age=" + age +
'}';
}
}
重要提示
实现Externalizable的类必须提供public无参构造函数,否则反序列化时会抛出InvalidClassException。
学习目标
掌握serialVersionUID的作用与版本兼容性策略
serialVersionUID的作用
serialVersionUID用于标识序列化类的版本,确保序列化与反序列化过程中类的兼容性。
警告: 未显式声明serialVersionUID时,JVM会根据类结构自动生成一个ID。类结构变更会导致ID变化,引发InvalidClassException。
版本变更兼容策略
变更类型 |
兼容性 |
解决方案 |
添加字段 |
兼容 |
新字段初始化为默认值 |
删除字段 |
兼容 |
多余字段被忽略 |
修改字段类型 |
不兼容 |
需自定义readObject()处理 |
修改字段名 |
不兼容 |
需自定义readObject()处理 |
修改类层次结构 |
可能不兼容 |
需仔细测试 |
处理不兼容变更
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
// 读取兼容字段
in.defaultReadObject();
// 处理新版本添加的字段
if (in.available() > 0) {
this.newField = in.readUTF();
} else {
this.newField = "default";
}
// 处理重命名字段
try {
// 旧版本字段名
this.renamedField = (String) in.readObject();
} catch (OptionalDataException e) {
// 字段不存在时的处理
this.renamedField = null;
}
}
学习目标
掌握序列化使用中的安全注意事项和性能优化策略
安全实践
- 使用
transient
标记敏感字段
- 对敏感字段进行加密
- 验证反序列化数据来源
- 使用白名单控制可反序列化的类
- 避免反序列化不可信数据源
性能优化
- 使用Externalizable替代Serializable提升性能
- 避免序列化大型对象图
- 使用对象池减少对象创建开销
- 考虑使用更高效的序列化库(如Protobuf、Kryo)
代码质量
- 始终显式声明serialVersionUID
- 为序列化类提供无参构造函数
- 重写toString()方法便于调试
- 实现equals()和hashCode()方法
- 添加版本兼容性处理逻辑
常见错误及解决方案
错误类型 |
原因 |
解决方案 |
NotSerializableException |
类未实现Serializable接口 |
实现Serializable接口 |
InvalidClassException |
serialVersionUID不匹配 |
显式声明serialVersionUID |
OptionalDataException |
数据格式不匹配 |
检查序列化/反序列化逻辑 |
StreamCorruptedException |
流数据损坏 |
验证数据源完整性 |
内存泄漏 |
对象引用未释放 |
使用try-with-resources关闭流 |
错误处理示例
public class SerializationErrorHandler {
public static Object safeDeserialize(String filename) {
try (FileInputStream fileIn = new FileInputStream(filename);
ObjectInputStream in = new ObjectInputStream(fileIn)) {
return in.readObject();
} catch (InvalidClassException e) {
System.err.println("版本不兼容: " + e.getMessage());
// 尝试兼容处理
return handleVersionMismatch(filename);
} catch (ClassNotFoundException e) {
System.err.println("类未找到: " + e.getMessage());
// 使用替代类
return useAlternativeClass(filename);
} catch (IOException e) {
System.err.println("IO错误: " + e.getMessage());
return null;
}
}
private static Object handleVersionMismatch(String filename) {
// 实现版本兼容逻辑
return null;
}
private static Object useAlternativeClass(String filename) {
// 使用替代类进行反序列化
return null;
}
}
对象缓存实现
import java.io.*;
import java.util.*;
public class ObjectCache {
private final String cacheDir;
private final Map
index = new HashMap<>();
public ObjectCache(String cacheDir) {
this.cacheDir = cacheDir;
File dir = new File(cacheDir);
if (!dir.exists()) {
dir.mkdirs();
}
loadIndex();
}
/**
* 将对象存入缓存
* @param key 缓存键
* @param obj 缓存对象
*/
public void put(String key, Serializable obj) {
String filename = getFilename(key);
try (ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream(filename))) {
out.writeObject(obj);
index.put(key, System.currentTimeMillis());
saveIndex();
} catch (IOException e) {
System.err.println("缓存写入失败: " + e.getMessage());
}
}
/**
* 从缓存获取对象
* @param key 缓存键
* @return 缓存对象
*/
public Object get(String key) {
String filename = getFilename(key);
if (!new File(filename).exists()) {
return null;
}
try (ObjectInputStream in = new ObjectInputStream(
new FileInputStream(filename))) {
Object obj = in.readObject();
index.put(key, System.currentTimeMillis()); // 更新访问时间
saveIndex();
return obj;
} catch (IOException | ClassNotFoundException e) {
System.err.println("缓存读取失败: " + e.getMessage());
return null;
}
}
/**
* 清理过期缓存
* @param maxAge 最大存活时间(毫秒)
*/
public void cleanup(long maxAge) {
long now = System.currentTimeMillis();
Iterator> it = index.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = it.next();
if (now - entry.getValue() > maxAge) {
String filename = getFilename(entry.getKey());
new File(filename).delete();
it.remove();
}
}
saveIndex();
}
private String getFilename(String key) {
return cacheDir + File.separator + key + ".ser";
}
private void saveIndex() {
try (ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream(cacheDir + File.separator + "index.ser"))) {
out.writeObject(index);
} catch (IOException e) {
System.err.println("索引保存失败: " + e.getMessage());
}
}
@SuppressWarnings("unchecked")
private void loadIndex() {
File indexFile = new File(cacheDir + File.separator + "index.ser");
if (indexFile.exists()) {
try (ObjectInputStream in = new ObjectInputStream(
new FileInputStream(indexFile))) {
index.putAll((Map) in.readObject());
} catch (IOException | ClassNotFoundException e) {
System.err.println("索引加载失败: " + e.getMessage());
}
}
}
}