mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1191 字
3 分钟
Java序列化与反序列化详解
2026-02-02

serialVersionUID不写会怎样?transient关键字有什么用?序列化这些看似简单的细节,搞不好就是线上事故的导火索。有次我改了一个实体类的字段,结果线上反序列化全崩了,就是因为没定义serialVersionUID。这篇文章把序列化的方方面面都讲清楚了。

Java序列化与反序列化详解#

序列化是干啥的?#

序列化就是把内存里的Java对象转成字节序列,方便存到文件里或者通过网络发出去。反序列化就是反过来,把字节序列恢复成对象。

Java自带的序列化用起来很方便,但坑也不少——serialVersionUIDtransient、性能问题,不注意就会出大事。

常见使用场景

  • 对象持久化到文件
  • 网络传输对象(RPC、分布式系统)
  • 缓存(Redis存对象)
  • Session序列化

Serializable接口(最常用)#

基本用法#

实现Serializable接口就能序列化了,啥额外代码都不用写——这就是个标记接口,告诉JVM这个类可以序列化:

import java.io.*;
public class Person implements Serializable {
private String name;
private int age;
private transient String password; // 这个字段不会被序列化
// 构造方法、getter/setter 省略
}
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("person.ser"))) {
Person person = new Person("张三", 25, "123456");
oos.writeObject(person);
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("person.ser"))) {
Person person = (Person) ois.readObject();
System.out.println(person);
}

serialVersionUID —— 这玩意儿不写会出事#

每个可序列化的类都有一个版本号serialVersionUID。反序列化的时候,JVM会对比当前类的UID和序列化数据里的UID,不匹配就抛InvalidClassException

为啥一定要显式声明?

如果你不写,JVM会根据类结构自动生成一个。但问题是——这个自动生成的UID对类的变化非常敏感,你只是加了一个字段,生成的UID就变了。然后线上在跑的服务,新版本反序列化旧版本的数据,直接就崩了。

public class Person implements Serializable {
// 显式声明,建议用private static final long
private static final long serialVersionUID = 1L;
private String name;
private int age;
// 加个新字段,不影响反序列化旧数据
private String email;
}

实战经验:我一般用1L作为初始值,每次改类结构(加字段、删字段)时手动更新一下。如果只是加字段,旧数据反序列化时新字段会被赋默认值(null、0等),不会报错。

transient —— 不想序列化的字段就标记它#

有些字段不该被序列化,比如密码、密钥、临时缓存:

private transient String password; // 反序列化后是null
private transient ThreadLocal<String> context; // 线程相关,没必要序列化

static字段不参与序列化#

静态字段属于类,不属于对象,序列化只保存对象的状态:

private static String staticField; // 反序列化后还是当前JVM中的值

Externalizable接口(更灵活但更麻烦)#

如果默认的序列化机制满足不了你(比如只想序列化部分字段,或者想加密后再序列化),可以实现Externalizable接口,手动控制读写逻辑:

public class Person implements Externalizable {
private String name;
private int age;
private String password;
// 必须有无参构造方法!
public Person() {}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
// 密码不序列化
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
// 密码不会被反序列化
}
}

注意:实现Externalizable必须提供public的无参构造方法,反序列化时通过反射调用它创建对象,再填充字段。

自定义序列化(更优雅的方式)#

既想用Serializable的自动机制,又想对某些字段做特殊处理,可以用writeObjectreadObject方法:

public class Person implements Serializable {
private String name;
private int age;
private transient String password;
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 先走默认序列化
// 密码加密后写入
String encrypted = encrypt(password);
out.writeUTF(encrypted);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 先走默认反序列化
// 读取加密的密码并解密
String encrypted = in.readUTF();
password = decrypt(encrypted);
}
}

序列化代理模式(安全又高效)#

这是Effective Java里推荐的做法——用一代理类来代替原始类进行序列化:

public class Person implements Serializable {
private String name;
private int age;
// 代理类
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
public SerializationProxy(Person p) {
this.name = p.name;
this.age = p.age;
}
private Object readResolve() {
return new Person(name, age);
}
}
// 序列化时用代理对象代替
private Object writeReplace() {
return new SerializationProxy(this);
}
// 防止直接反序列化
private void readObject(ObjectInputStream in) throws InvalidObjectException {
throw new InvalidObjectException("Use SerializationProxy instead");
}
}

好处是可以在代理类里做校验,而且原始类的内部字段变化不影响序列化格式。

序列化过滤器(Java 9+)#

防止反序列化攻击——只允许反序列化指定包下的类:

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"java.base/*;com.example.*;!*");
ObjectInputFilter.Config.setSerialFilter(filter);

实战贴士#

  1. 每个可序列化类都显式声明serialVersionUID:不要依赖JVM自动生成,否则改个字段就崩了
  2. 敏感字段用transient标记:密码、密钥、token等千万别序列化
  3. 考虑JSON替代:如果只是存文件或网络传输,用Jackson/Gson序列化成JSON比Java自带序列化更灵活
  4. 注意反序列化攻击:不要反序列化不可信的数据,或者用序列化过滤器做白名单
  5. Externalizable需要无参构造:这个很容易忘,忘了就报错

常见坑#

  • serialVersionUID不匹配:改了类结构没更新UID,反序列化直接报错。我亲身经历——改了一个实体类加了字段,线上反序列化全崩了,回滚后才恢复,从此老老实实写UID。
  • transient搞错了:标记了不该标记的字段,反序列化后数据丢了。
  • 反序列化攻击:恶意构造的序列化数据可以执行任意代码,著名的Fastjson漏洞就是这个原理。
  • 序列化性能差:Java原生序列化性能很差,大数据量场景用Protobuf或Kryo。
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

部分信息可能已经过时

目录