源本科技 | 码上会

Java 序列化与反序列化

2025/12/26
36
0

对象持久化的关键技术

在 Java 开发中,序列化(Serialization)反序列化(Deserialization) 是实现对象持久化、跨系统通信和缓存机制的核心技术。即使你未曾显式使用,它们也广泛存在于 Web 应用、RPC 框架、分布式系统等场景中。


学习目标

  • 理解序列化与反序列化的定义与作用

  • 掌握 Serializable 接口的使用规则

  • 了解哪些字段会被 / 不会被序列化

  • 熟悉 serialVersionUID 的作用与生成方式

  • 能够编写完整的序列化 / 反序列化示例代码

  • 区分序列化与其他对象复制机制(如克隆)


什么是序列化与反序列化

序列化

Java 对象的状态 转换为 字节流(byte stream) 的过程,以便存储到文件、数据库或通过网络传输。

反序列化

字节流 重新转换为 内存中的 Java 对象,恢复其原始状态。

关键特性:平台无关性 —— 在 Windows 上序列化的对象,可在 Linux 或 macOS 上成功反序列化。


如何使一个类支持序列化

要让一个类的对象可被序列化,必须实现 java.io.Serializable 接口

import java.io.Serializable;

class Student implements Serializable {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

关于 Serializable 接口

  • 它是一个 标记接口(Marker Interface):没有方法或字段

  • 仅用于“标记”该类允许被序列化

  • 其他标记接口:CloneableRemote

如果类未实现 Serializable,调用 writeObject() 会抛出 NotSerializableException


序列化与反序列化核心 API

操作

方法

说明

序列化

ObjectOutputStream

writeObject(Object obj)

将对象写入输出流

反序列化

ObjectInputStream

readObject()

从输入流读取并重建对象

保存与恢复对象

import java.io.*;

class Demo implements Serializable {
    public int a;
    public String b;

    public Demo(int a, String b) {
        this.a = a;
        this.b = b;
    }
}

public class MainApp {
    public static void main(String[] args) {
        Demo obj = new Demo(1, "codehub");
        String filename = "data.ser";

        // 序列化
        try (FileOutputStream fos = new FileOutputStream(filename);
             ObjectOutputStream oos = new ObjectOutputStream(fos)) {
            oos.writeObject(obj);
            System.out.println("对象已序列化");
        } catch (IOException e) {
            System.err.println("序列化失败: " + e.getMessage());
        }

        // 反序列化
        try (FileInputStream fis = new FileInputStream(filename);
             ObjectInputStream ois = new ObjectInputStream(fis)) {
            Demo restored = (Demo) ois.readObject();
            System.out.println("对象已反序列化");
            System.out.println("a = " + restored.a);  // 输出:1
            System.out.println("b = " + restored.b);  // 输出:codehub
        } catch (IOException | ClassNotFoundException e) {
            System.err.println("反序列化失败: " + e.getMessage());
        }
    }
}

使用 try-with-resources 自动关闭流,避免资源泄漏。


哪些会被序列化?哪些不会?

字段类型

是否参与序列化

说明

普通实例变量

✅ 是

String name, int age

静态变量(static)

❌ 否

属于类,不属于对象实例

瞬态变量(transient)

❌ 否

显式排除,反序列化时设为默认值(如 0, null

final 变量

✅ 是

即使是 final,只要不是 static,仍会被序列化

瞬态变量与静态变量的行为

class Emp implements Serializable {
    private static final long serialVersionUID = 1L;
    
    transient int secret;     // 不会被序列化
    static int counter = 100; // 不会被序列化
    String name;
    int age;

    public Emp(String name, int age, int secret) {
        this.name = name;
        this.age = age;
        this.secret = secret;
        counter++; // 静态变量,所有实例共享
    }
}

public class TestTransient {
    public static void main(String[] args) throws Exception {
        Emp e1 = new Emp("张三", 25, 999);
        System.out.println("序列化前: secret=" + e1.secret + ", counter=" + Emp.counter);

        // 序列化
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("emp.ser"))) {
            oos.writeObject(e1);
        }

        // 修改静态变量
        Emp.counter = 2000;

        // 反序列化
        Emp e2;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("emp.ser"))) {
            e2 = (Emp) ois.readObject();
        }

        System.out.println("反序列化后: secret=" + e2.secret + ", counter=" + Emp.counter);
    }
}

输出:

序列化前: secret=999, counter=101
反序列化后: secret=0, counter=2000

解释:

  • secrettransient,反序列化后为默认值 0

  • counterstatic,反序列化时不恢复,而是使用当前类中的值(2000)


serialVersionUID

版本控制的关键

为什么需要它?

当类结构发生变化(如新增字段),若未显式声明 serialVersionUID,JVM 会自动生成一个基于类结构的 UID。一旦类变更,UID 改变,反序列化将失败(抛出 InvalidClassException)。

正确做法:显式声明

private static final long serialVersionUID = 1L;
  • 必须是 static final long 类型

  • 建议使用 private 修饰(不被继承)

  • 值可任意指定(通常用 1L 或时间戳)

使用 serialver 工具生成 UID

注意:也可以使用插件生成

JDK 自带工具,可查看已有类的 UID:

serialver com.example.MyClass

最佳实践:所有可序列化类都应显式声明 serialVersionUID,避免因编译器差异导致兼容性问题。


注意事项

1. 构造函数不会被调用

反序列化时,不会调用任何构造函数,包括无参构造函数。对象是通过反射直接分配内存并填充字段的。

2. 继承关系中的序列化

  • 若父类实现了 Serializable,子类自动可序列化

  • 若子类可序列化但父类不可,则父类必须有无参构造函数(用于初始化非序列化部分)

3. 关联对象也需可序列化

如果对象包含其他对象引用(如 List<User>),这些关联对象也必须实现 Serializable,否则会抛出异常。


transientfinal 的特殊组合

即使将 final 字段标记为 transient它仍会被序列化!因为 final 字段在编译期常被内联(inline),其值直接嵌入字节码。

示例验证

class Dog implements Serializable {
    int i = 10;
    transient final int j = 20; // 看似不会序列化,实际会!
}

public class FinalTransientTest {
    public static void main(String[] args) throws Exception {
        Dog d1 = new Dog();
        
        // 序列化
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dog.ser"))) {
            oos.writeObject(d1);
        }
        
        // 反序列化
        Dog d2;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dog.ser"))) {
            d2 = (Dog) ois.readObject();
        }
        
        System.out.println(d2.i + "\t" + d2.j); // 输出:10	20
    }
}

结论:final 字段无论是否 transient,都会被序列化(因其值在编译时已确定)。


序列化 vs 反序列化

特性

序列化

反序列化

方向

对象 → 字节流

字节流 → 对象

用途

持久化、网络传输

恢复对象状态

关键类

ObjectOutputStream

ObjectInputStream

方法

writeObject()

readObject()

异常

IOException

IOException, ClassNotFoundException


重点总结

  • 序列化 = 对象 → 字节流;反序列化 = 字节流 → 对象

  • 类必须实现 Serializable 接口才能被序列化

  • statictransient 字段 不会被序列化

  • 务必显式声明 serialVersionUID 以保证版本兼容性

  • 反序列化时 不调用构造函数

  • 关联对象也必须可序列化,否则失败

  • final 字段即使标记 transient 也会被序列化(因编译期内联)


思考题

  1. 如果一个类实现了 Serializable,但它的某个字段是不可序列化的第三方类对象,该如何处理?

  2. 为什么反序列化时不调用构造函数?这对对象初始化有何影响?

  3. 在微服务架构中,序列化常用于哪些场景?除了 Java 原生序列化,还有哪些更高效的替代方案(如 JSON、Protobuf)?