文章目录

Java 序列化:Serializable 与 Externalizable

发布于 2026-04-05 16:57:39 · 浏览 14 次 · 评论 0 条

Java 序列化:Serializable 与 Externalizable

序列化是 Java 开发中不可或缺的基础机制,它让对象能够脱离 JVM 运行环境持久保存,或在网络中高效传输。然而,很多开发者对序列化的理解仅限于给类加上 Serializable 接口,对序列化的底层机制和控制手段知之甚少。本文将深入剖析 Java 序列化的两种核心方式,帮你彻底掌握这门技术。


什么是序列化?

序列化是将 Java 对象转换为字节流的过程,反序列化则是将字节流恢复为对象的过程。这一机制解决了两个核心问题:对象持久化(将对象保存到磁盘或数据库)和对象传输(在网络中将对象从一个 JVM 发送到另一个 JVM)。

Java 提供了两种序列化方式:自动序列化手动序列化。前者通过 Serializable 接口实现,后者通过 Externalizable 接口实现。理解两者的差异,是写出高效、可靠的序列化代码的前提。


Serializable 接口:自动序列化

基本用法

Serializable 是一个标记接口(Marker Interface),它不包含任何方法声明。实现该接口的类,其对象将被 Java 序列化机制自动处理。

import java.io.*;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    private transient String password; // 被 transient 修饰的字段不参与序列化

    // 构造方法、getter、setter
    public User(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + ", password='" + password + "'}";
    }
}

执行序列化与反序列化:

public class SerializableDemo {
    public static void main(String[] args) {
        User user = new User("张三", 25, "secret123");

        // 序列化:对象 → 字节流
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("user.ser"))) {
            oos.writeObject(user);
            System.out.println("序列化完成");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化:字节流 → 对象
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("user.ser"))) {
            User restoredUser = (User) ois.readObject();
            System.out.println("反序列化结果: " + restoredUser);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

输出:

序列化完成
反序列化结果: User{name='张三', age=25, password='null'}

注意 password 字段因为被 transient 修饰,反序列化后值为 null。这是控制序列化行为的最简单方式。

serialVersionUID 的重要性

每个可序列化的类都有一个版本标识符 serialVersionUID。反序列化时,JVM 会比对字节流中的 UID 与类中定义的 UID 是否一致,不一致则抛出 InvalidClassException

// 强烈建议显式声明 serialVersionUID
private static final long serialVersionUID = 20240701L;

如果不显式声明,JVM 会根据类的成员结构自动生成一个 UID。这意味着:当类结构发生变化(如添加字段),旧版本序列化出的字节流将无法被新版本的类正确反序列化。显式声明 UID 可以让类在结构变化后仍然兼容旧数据。


Externalizable 接口:完全控制序列化

Externalizable 接口继承自 Serializable,但它要求开发者完全自己控制序列化和反序列化的过程。实现该接口的类必须实现两个方法:writeExternal()readExternal()

基本用法

import java.io.*;

public class Product implements Externalizable {
    private String name;
    private double price;
    private int stock;

    // Externalizable 要求必须有一个无参构造方法
    public Product() {}

    public Product(String name, double price, int stock) {
        this.name = name;
        this.price = price;
        this.stock = stock;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // 完全自定义序列化逻辑
        out.writeUTF(name);           // 只序列化 name
        out.writeDouble(price);       // 只序列化 price
        // 注意:stock 字段被跳过,不参与序列化
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // 必须严格按照 writeExternal 的顺序读取
        this.name = in.readUTF();
        this.price = in.readDouble();
        // stock 保持默认值 0
    }

    @Override
    public String toString() {
        return "Product{name='" + name + "', price=" + price + ", stock=" + stock + "}";
    }
}

执行序列化与反序列化:

public class ExternalizableDemo {
    public static void main(String[] args) {
        Product product = new Product("笔记本电脑", 6999.00, 100);

        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("product.ser"))) {
            oos.writeObject(product);
            System.out.println("序列化完成");
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("product.ser"))) {
            Product restoredProduct = (Product) ois.readObject();
            System.out.println("反序列化结果: " + restoredProduct);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

输出:

序列化完成
反序列化结果: Product{name='笔记本电脑', price=6999.0, stock=0}

stock 字段在 writeExternal() 中没有被写入,因此反序列化后为默认值 0。这展示了 Externalizable 提供的细粒度控制能力。

关键细节

使用 Externalizable 时必须注意以下几点:

无参构造方法是必需的。反序列化时,JVM 会先调用无参构造方法创建对象,然后再调用 readExternal() 填充数据。如果没有无参构造方法,将抛出 InvalidClassException

读写顺序必须严格一致readExternal() 中读取字段的顺序、数据类型必须与 writeExternal() 中写入的顺序完全匹配,否则数据会错乱。


核心差异对比

特性 Serializable Externalizable
接口类型 标记接口 普通接口(需实现两个方法)
控制粒度 粗粒度(只能通过 transient 排除字段) 细粒度(完全自定义序列化逻辑)
性能 一般(反射机制,速度较慢) 优秀(直接调用方法,无反射开销)
安全性 低(自动序列化可能泄露敏感数据) 高(可精确控制哪些数据被序列化)
版本兼容性 依赖 serialVersionUID 同左,但需自行处理字段加减
实现复杂度 低(只需实现接口) 高(需自行编写序列化逻辑)

实战场景与选型建议

场景一:简单数据传输

如果对象结构简单,只需要持久化或传输基本数据,且对性能要求不高,选择 Serializable。它实现简单、维护成本低,适合大多数场景。

场景二:高性能要求

如果序列化是系统性能瓶颈(如高频交易系统、大数据处理),Externalizable 是更好的选择。它避免了反射开销,可以写出针对性的高效序列化代码。

场景三:敏感数据处理

如果对象包含密码、密钥等敏感字段,Externalizable 允许你精确控制只序列化必要字段,避免敏感数据泄露。

场景四:复杂版本演进

如果类结构会频繁变化,且需要保持与旧版本数据的兼容性,Externalizable 让你可以在读写时进行版本判断和处理,实现平滑升级。


最佳实践总结

始终显式声明 serialVersionUID。即使当前不需要考虑版本兼容,显式声明也是一个好习惯,它让类的演化更加可控。

使用 transient 保护敏感数据。密码、令牌、私钥等字段应该标记为 transient,确保它们不会被意外序列化。

优先选择 Serializable。除非有明确的性能或安全需求,否则 Serializable 是更实用的选择。过度优化是万恶之源。

Externalizable 中验证数据readExternal() 是反序列化入口,可以在这里添加数据校验逻辑,增强系统健壮性。

考虑使用替代方案。JSON(如 Jackson、Gson)、Protocol Buffers、Kyro 等现代序列化框架在性能、跨语言支持、可读性方面往往优于原生 Java 序列化。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文