⭐ 推荐:社区规则条款 V1.0

Java 的序列化?

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-21 11:39 ·7 浏览 ·0 回复
内容摘要

介绍 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 兜住;永远不要反序列化不可信来源的数据。
本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2346.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~