Java 的序列化?
介绍 Java 序列化的最小可运行示例与基本规则:类实现 Serializable 即可将对象写入文件并还原,需显式声明 static final long serialVersionUID 以避免版本不兼容,static 等字段不会随对象
学完这篇你能得到一个能跑起来的最小示例、几条必须记住的规则,以及上线前应该避开的坑——用不上高深技巧,照着做就行。
第一步:先搞清楚序列化在做什么,写出第一个能跑的示例
这一步的目标是把一个 Java 对象变成字节流,再把它还原回来,让你亲眼看到“对象被存下来了”。
序列化指的是把一个对象的状态转成一串字节,反序列化则是把这串字节还原成对象。要用它,类必须实现 java.io.Serializable 接口——这是个空接口,只起标记作用,不包含任何方法。
写两个类试一下:
import java.io.*;
class User implements Serializable {
private String name;
private int age;
User(String name, int age) { this.name = name; this.age = age; }
@Override
public String toString() { return "User{name=" + name + ", age=" + age + "}"; }
}
public class Demo {
public static void main(String[] args) throws Exception {
User u = new User("张三", 28);
// 序列化:对象 -> 字节 -> 文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
oos.writeObject(u);
}
// 反序列化:文件 -> 字节 -> 对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.bin"))) {
User back = (User) ois.readObject();
System.out.println(back);
}
}
}
在 JDK 8 到 JDK 21 上这段代码都能直接编译运行(javac Demo.java && java Demo),控制台会打印 User{name=张三, age=28}。看到这行输出,说明对象确实被存进了文件又被读了回来。
第二步:加上 serialVersionUID,避免“改一行代码就报错”
这一步要解决的是版本兼容问题:给类加一个固定的版本号,以后小改动就不会导致反序列化失败。
反序列化时,JVM 会对比字节流里的版本号和当前类的版本号。不显式声明的话,编译器会根据类的结构自动生成一个,你哪怕只加一个方法,这个值也会变,然后用旧文件反序列化就会抛 java.io.InvalidClassException: ... local class incompatible。
class User implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}
注意:
serialVersionUID必须是static final long,少一个修饰符都不会被识别,编译器也不会报错,于是问题会在运行时才暴露。
第三步:明确哪些字段不会跟着走
这一步是为了让你在设计类时就知道哪些数据会丢。
static字段属于类不属于对象,不会被序列化。transient字段会被跳过,反序列化后是默认值(int是 0,对象是null)。- 父类没有实现
Serializable时,父类的字段不会被序列化;而且反序列化时,需要调用父类的无参构造器。
class Account implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private transient String password; // 不会被写进字节流
}
注意:密码、密钥、token 这类敏感字段一定要加
transient;否则它们会被明文写进文件或网络流,事后很难追查。
第四步:需要精细控制时,用 writeObject / readObject
这一步解决的是“某些字段想加密、想转换格式再存”的需求。
在你的类里自己写这两个私有方法,序列化框架会调用它们:
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 先按默认规则写
out.writeObject(encrypt(password)); // 再补一个加密后的值
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.password = decrypt((String) in.readObject());
}
方法签名必须严格是 private void writeObject(ObjectOutputStream) 和 private void readObject(ObjectInputStream),写错修饰符相当于没写。
第五步:上线前必须知道的几个坑
坑一:单例会被破坏。 反序列化不走构造器,会造出新实例。解决办法是在类里加 private Object readResolve() { return INSTANCE; }。
坑二:反序列化是不可信输入的入口。 readObject 会按字节流里的类名去加载类并执行代码,攻击者可以构造恶意字节流。生产环境不要反序列化来路不明的数据,也不要用 ObjectInputStream 直接对外暴露接口。
坑三:性能与体积。 Java 原生序列化产出的字节流偏大、速度一般。跨语言或高并发场景,通常换 JSON(Jackson、Gson)或 Protobuf 更合适——原生序列化更适合纯 Java 内部的临时持久化或 RPC。
注意:
Externalizable接口虽然也能用,但它要求类有无参构造器,且完全由你手写读写逻辑,除非有特殊需求,一般不建议选它。
小结
- 序列化类必须
implements Serializable,用ObjectOutputStream.writeObject写、ObjectInputStream.readObject读。 - 显式声明
private static final long serialVersionUID = 1L;,否则类一改就反序列化失败。 static和transient字段不会被序列化,敏感字段一律加transient。- 需要自定义逻辑就写私有的
writeObject/readObject,配合defaultWriteObject()。 - 单例要用
readResolve兜住;永远不要反序列化不可信来源的数据。
转载请注明出处,版权归原作者所有。