serialVersionUID不写会怎样?transient关键字有什么用?序列化这些看似简单的细节,搞不好就是线上事故的导火索。有次我改了一个实体类的字段,结果线上反序列化全崩了,就是因为没定义serialVersionUID。这篇文章把序列化的方方面面都讲清楚了。
Java序列化与反序列化详解
序列化是干啥的?
序列化就是把内存里的Java对象转成字节序列,方便存到文件里或者通过网络发出去。反序列化就是反过来,把字节序列恢复成对象。
Java自带的序列化用起来很方便,但坑也不少——serialVersionUID、transient、性能问题,不注意就会出大事。
常见使用场景:
- 对象持久化到文件
- 网络传输对象(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; // 反序列化后是nullprivate 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的自动机制,又想对某些字段做特殊处理,可以用writeObject和readObject方法:
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);实战贴士
- 每个可序列化类都显式声明serialVersionUID:不要依赖JVM自动生成,否则改个字段就崩了
- 敏感字段用transient标记:密码、密钥、token等千万别序列化
- 考虑JSON替代:如果只是存文件或网络传输,用Jackson/Gson序列化成JSON比Java自带序列化更灵活
- 注意反序列化攻击:不要反序列化不可信的数据,或者用序列化过滤器做白名单
- Externalizable需要无参构造:这个很容易忘,忘了就报错
常见坑
- serialVersionUID不匹配:改了类结构没更新UID,反序列化直接报错。我亲身经历——改了一个实体类加了字段,线上反序列化全崩了,回滚后才恢复,从此老老实实写UID。
- transient搞错了:标记了不该标记的字段,反序列化后数据丢了。
- 反序列化攻击:恶意构造的序列化数据可以执行任意代码,著名的
Fastjson漏洞就是这个原理。 - 序列化性能差:Java原生序列化性能很差,大数据量场景用Protobuf或Kryo。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时






