原创

Java序列化和反序列化教程:从入门到实践

什么是 Java 序列化与反序列化

Java 序列化,本质上是把内存中的对象状态转换成可传输、可存储的字节序列;反序列化则是把这些字节序列重新恢复成 Java 对象。

这套机制最早的典型用途有三类:

  • 对象持久化到磁盘
  • 对象通过网络传输
  • 对象在分布式系统、缓存系统、消息系统之间交换

最常见的内置实现来自 java.io.SerializableObjectOutputStream / ObjectInputStream

先看一个最小示例。

import java.io.Serializable;

public class User implements Serializable {
    private String name;
    private int age;

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

    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + "}";
    }
}
import java.io.*;

public class SerializeDemo {
    public static void main(String[] args) throws Exception {
        User user = new User("Alice", 28);

        // 序列化
        try (ObjectOutputStream oos =
                     new ObjectOutputStream(new FileOutputStream("user.obj"))) {
            oos.writeObject(user);
        }

        // 反序列化
        try (ObjectInputStream ois =
                     new ObjectInputStream(new FileInputStream("user.obj"))) {
            User result = (User) ois.readObject();
            System.out.println(result);
        }
    }
}

运行后,user 对象会被写入 user.obj 文件,再从文件中读回。


为什么 Java 需要序列化

很多初学者会把序列化理解成“把对象保存一下”,这个理解不算错,但不完整。更准确地说,序列化的核心价值是:让对象脱离当前 JVM 进程而存在

例如:

1. 远程调用与网络传输

客户端与服务端之间无法直接共享内存对象,所以必须把对象转换成某种可传输格式。

2. 缓存与会话复制

例如将会话对象写入 Redis、磁盘或其他节点,需要先完成序列化。

3. 对象快照与持久化

某些场景下,需要把对象当前状态落盘,以便恢复运行现场。


Java 内置序列化的基本原理

当一个类实现 Serializable 接口后,JVM 就认为它的对象可以参与默认序列化流程。

public class User implements Serializable {
}

这个接口本身没有任何方法,它只是一个标记接口。JVM 在执行 ObjectOutputStream.writeObject() 时,会检查对象是否实现了它。

默认序列化时,JVM 会处理对象中的非静态、非 transient 修饰字段,并把对象图一并写出。这里的“对象图”很关键,意思是如果一个对象引用了其他对象,JVM 会继续递归序列化这些被引用对象。

例如:

import java.io.Serializable;

class Address implements Serializable {
    private String city;
    private String street;

    public Address(String city, String street) {
        this.city = city;
        this.street = street;
    }

    @Override
    public String toString() {
        return city + " " + street;
    }
}

class User implements Serializable {
    private String name;
    private Address address;

    public User(String name, Address address) {
        this.name = name;
        this.address = address;
    }

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

如果 User 要成功序列化,那么它引用的 Address 也必须可序列化,否则运行时会抛出 NotSerializableException


Serializable 的作用与特点

Serializable 的使用门槛很低,但也正因为太低,很多问题会被忽略。

它的几个关键特点如下:

1. 使用简单

实现接口即可,不需要手动写编码和解码逻辑。

2. 强依赖 Java 平台

Java 原生序列化生成的是 Java 专用二进制格式,可读性差,也不适合跨语言系统对接。

3. 性能通常不理想

相较于 JSON、ProtoBuf、Kryo 等方案,Java 原生序列化通常体积更大、速度更慢。

4. 存在安全风险

反序列化如果处理不可信数据,可能触发安全漏洞。这是 Java 原生反序列化被频繁批评的根本原因。


serialVersionUID 到底有什么用

这是 Java 序列化里最容易被忽视、但又最关键的点之一。

看下面的类:

import java.io.Serializable;

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

    private String name;
    private int age;
}

serialVersionUID 可以理解为类序列化版本号。反序列化时,JVM 会校验“字节流中记录的版本号”和“当前类定义中的版本号”是否一致。

如果不一致,就会抛出 InvalidClassException

为什么建议显式声明

如果不手动声明,JVM 会根据类结构自动生成一个值。问题在于,只要类结构发生变化,例如:

  • 增加字段
  • 删除字段
  • 修改字段类型
  • 修改类定义结构

自动生成的 serialVersionUID 就可能变化。这样老数据在反序列化时就可能失败。

最佳实践

通常建议所有可序列化类都显式声明:

private static final long serialVersionUID = 1L;

字段变更后一定要改版本号吗

不一定。

关键不是“改不改”,而是你是否允许旧数据继续兼容

  • 如果只是新增字段,并且希望兼容旧数据,通常可以不改
  • 如果类结构变化已经破坏兼容性,应该修改版本号,明确拒绝旧数据反序列化

哪些字段不会被序列化

默认情况下,以下内容不会被序列化:

1. static 字段

静态字段属于类,不属于对象状态,因此不会被序列化。

2. transient 字段

transient 修饰的字段会被显式排除。

示例:

import java.io.Serializable;

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

    private String username;
    private transient String password;
    private static String type = "NORMAL";

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

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

序列化再反序列化后:

  • username 会恢复
  • password 会变成默认值 null
  • type 根本不属于对象序列化内容

transient 的典型使用场景

  • 密码
  • 密钥
  • 临时计算结果
  • 不可序列化资源句柄
  • 不希望落盘的敏感信息

完整示例:序列化单个对象

下面给出一个稍完整的例子。

import java.io.Serializable;

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

    private Long id;
    private String name;
    private Integer age;

    public Student(Long id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{id=" + id + ", name='" + name + "', age=" + age + "}";
    }
}
import java.io.*;

public class StudentSerializeExample {

    public static void main(String[] args) {
        Student student = new Student(1L, "Tom", 20);
        String fileName = "student.ser";

        serialize(student, fileName);
        Student result = deserialize(fileName);

        System.out.println(result);
    }

    public static void serialize(Student student, String fileName) {
        try (ObjectOutputStream oos =
                     new ObjectOutputStream(new FileOutputStream(fileName))) {
            oos.writeObject(student);
            System.out.println("对象序列化成功");
        } catch (IOException e) {
            throw new RuntimeException("序列化失败", e);
        }
    }

    public static Student deserialize(String fileName) {
        try (ObjectInputStream ois =
                     new ObjectInputStream(new FileInputStream(fileName))) {
            System.out.println("对象反序列化成功");
            return (Student) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException("反序列化失败", e);
        }
    }
}

完整示例:序列化集合对象

集合本身只要其实现类可序列化,也可以直接写出。例如 ArrayList 本身就支持序列化。

import java.io.Serializable;

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

    private Long id;
    private String name;
    private Double price;

    public Product(Long id, String name, Double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    @Override
    public String toString() {
        return "Product{id=" + id + ", name='" + name + "', price=" + price + "}";
    }
}
import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class ProductListSerializeDemo {

    public static void main(String[] args) {
        List<Product> products = new ArrayList<>();
        products.add(new Product(101L, "Keyboard", 199.0));
        products.add(new Product(102L, "Mouse", 99.0));

        String fileName = "products.ser";

        try (ObjectOutputStream oos =
                     new ObjectOutputStream(new FileOutputStream(fileName))) {
            oos.writeObject(products);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        try (ObjectInputStream ois =
                     new ObjectInputStream(new FileInputStream(fileName))) {
            List<Product> result = (List<Product>) ois.readObject();
            result.forEach(System.out::println);
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

前提仍然一样:集合里的元素也必须可序列化。


自定义序列化:writeObject 与 readObject

默认序列化不是唯一方式。某些情况下,你希望控制字段写入逻辑,例如:

  • 某些字段需要加密
  • 某些字段需要压缩
  • 某些字段需要组合后存储
  • 某些字段虽然是 transient,但仍希望手动处理

这时可以在类中定义私有方法:

  • private void writeObject(ObjectOutputStream out)
  • private void readObject(ObjectInputStream in)

示例:

import java.io.*;

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

    private String username;
    private transient String password;

    public Account(String username, String password) {
        this.username = username;
        this.password = password;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // 先序列化默认字段
        out.writeObject(password == null ? null : new StringBuilder(password).reverse().toString());
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject(); // 先读取默认字段
        String encrypted = (String) in.readObject();
        this.password = encrypted == null ? null : new StringBuilder(encrypted).reverse().toString();
    }

    @Override
    public String toString() {
        return "Account{username='" + username + "', password='" + password + "'}";
    }
}
import java.io.*;

public class CustomSerializeDemo {
    public static void main(String[] args) throws Exception {
        Account account = new Account("admin", "123456");

        try (ObjectOutputStream oos =
                     new ObjectOutputStream(new FileOutputStream("account.ser"))) {
            oos.writeObject(account);
        }

        try (ObjectInputStream ois =
                     new ObjectInputStream(new FileInputStream("account.ser"))) {
            Account result = (Account) ois.readObject();
            System.out.println(result);
        }
    }
}

这里的“加密”只是演示思路,实际生产环境不能使用这种字符串反转方式存储敏感数据。


不可序列化父类与可序列化子类

这是一个常见面试点,也容易在真实项目里踩坑。

结论先说清楚:

  • 如果父类没有实现 Serializable
  • 子类实现了 Serializable

那么子类对象仍然可以被序列化,但父类那部分状态不会按常规序列化机制处理,而是在反序列化时通过父类无参构造方法初始化。

示例:

public class Person {
    protected String name;

    public Person() {
        this.name = "default-name";
    }

    public Person(String name) {
        this.name = name;
    }
}
import java.io.Serializable;

public class Employee extends Person implements Serializable {
    private static final long serialVersionUID = 1L;

    private String department;

    public Employee(String name, String department) {
        super(name);
        this.department = department;
    }

    @Override
    public String toString() {
        return "Employee{name='" + name + "', department='" + department + "'}";
    }
}

如果反序列化后看到 name 不是原来的值,而是父类无参构造里初始化的值,不要意外,这就是机制决定的。

注意点

如果父类没有无参构造方法,反序列化时可能失败。


Externalizable 与 Serializable 的区别

除了 Serializable,Java 还提供了 Externalizable

它继承自 Serializable,但要求开发者显式实现两个方法:

  • writeExternal(ObjectOutput out)
  • readExternal(ObjectInput in)

示例:

import java.io.*;

public class Book implements Externalizable {
    private String name;
    private double price;

    public Book() {
        // 必须提供 public 无参构造
    }

    public Book(String name, double price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeDouble(price);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = (String) in.readObject();
        this.price = in.readDouble();
    }

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

两者区别可以概括为:


对比项 Serializable Externalizable
控制权 JVM 默认处理 开发者完全控制
实现难度
性能优化空间 较有限 更大
字段自动处理
必须有 public 无参构造

大多数业务项目里,Serializable 更常见;Externalizable 更像是一个“完全手动接管”的高级选项。


Java 序列化中的常见异常

1. NotSerializableException

最典型的错误,表示某个对象不支持序列化。

例如:

java.io.NotSerializableException: com.example.Address

出现原因通常是:

  • 当前类没有实现 Serializable
  • 成员字段引用的某个类没有实现 Serializable

2. InvalidClassException

通常与 serialVersionUID 不匹配有关。

例如类结构改了,但旧字节流还在,反序列化时就会报错。

3. EOFException

读取流时数据不完整,或者读写顺序不一致。

4. StreamCorruptedException

流内容被破坏,或不是合法的 Java 对象流格式。


Java 序列化的版本兼容问题

这是实际项目里比“能不能序列化”更重要的问题。

哪些变更通常相对安全

  • 新增普通字段
  • 删除某些非关键字段
  • 修改字段访问修饰符

哪些变更通常会破坏兼容性

  • 修改字段类型
  • 修改继承结构
  • 修改类名
  • 修改包名
  • 大幅改变对象语义

需要强调一点:“技术上能反序列化”不等于“业务上兼容”

例如你新增了一个订单状态字段,旧数据反序列化后该字段为默认值 null,程序虽然不报错,但业务逻辑可能已经错了。

所以版本兼容不只是 Java 机制问题,更是数据模型演进问题。


序列化在实际项目中的典型应用

1. 缓存对象

早期 Java 项目中,经常把对象直接序列化后存入 Redis 或文件缓存。但现在更常见的是 JSON、Hessian、Kryo、ProtoBuf 等方案。

2. Session 持久化

某些 Servlet 容器或集群环境下,会要求 Session 中的对象可序列化。

3. 消息传输

虽然 Java 原生序列化可以用于消息传输,但现代系统里通常不推荐这么做,因为可读性、兼容性与安全性都不理想。

4. 深拷贝

有些代码会通过“先序列化,再反序列化”的方式实现对象深拷贝。

例如:

import java.io.*;

public class DeepCopyUtil {

    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T deepCopy(T object) {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(object);

            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);

            return (T) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException("深拷贝失败", e);
        }
    }
}

这种方式简单,但性能一般,不适合高频场景。


为什么现在很多项目不推荐 Java 原生序列化

这不是说 Java 原生序列化“不能用”,而是说它在现代系统里通常不是优先选择。

主要原因有三点。

1. 性能不占优

对象流格式冗余较多,序列化后的字节体积通常偏大。

2. 跨语言能力差

如果系统包含 Java、Go、Python、Node.js 等多语言服务,Java 原生序列化几乎没有通用性。

3. 安全风险高

不要对不可信输入做原生反序列化。这是底线。

历史上很多反序列化漏洞,本质都是攻击者构造恶意字节流,诱导应用在反序列化过程中执行危险逻辑。


生产环境中的实践建议

1. 仅在必要时使用 Java 原生序列化

如果只是接口传输数据,优先考虑 JSON、ProtoBuf 等更通用的方案。

2. 所有可序列化类都显式声明 serialVersionUID

不要依赖 JVM 自动生成。

3. 敏感字段使用 transient

密码、令牌、临时凭证等不要默认写入字节流。

4. 不要反序列化不可信数据

来自外部用户、未知服务、可篡改存储介质的数据,都不能直接交给 ObjectInputStream

5. 谨慎对待类结构变更

尤其是已经持久化、已经跨版本传输的类,不要随意修改字段类型和继承结构。

6. 不要把序列化当成通用深拷贝工具

它只是可以这么用,不代表应该高频这么用。


JDK 版本下的理解重点

JDK 8 到 JDK 21 的核心机制是否变化很大

就日常开发视角看,Java 原生序列化的核心使用方式没有本质变化

  • 仍然基于 Serializable
  • 仍然使用 ObjectOutputStream / ObjectInputStream
  • serialVersionUID 仍然是兼容控制关键点
  • 安全风险依旧存在

版本差异应关注什么

真正需要关注的不是“语法会不会写”,而是:

  • 不同 JDK 版本下对安全防护、过滤机制的支持
  • 业务系统是否仍应继续使用原生序列化
  • 与新型序列化框架的替代关系

也就是说,原生序列化更像一个必须理解的基础能力,而不是现代架构中的默认首选方案


一个容易混淆的问题:JSON 转换算不算序列化

从广义概念上说,把对象转换为可存储、可传输格式,都可以叫序列化;把格式数据恢复成对象,都可以叫反序列化。

但在 Java 语境里,如果不加限定地说“序列化”,通常默认指的是:

  • Serializable
  • ObjectOutputStream
  • ObjectInputStream

而 Jackson、Gson 把对象转成 JSON,通常会明确说成:

  • JSON 序列化
  • JSON 反序列化

两者不要混为一谈。


面试和项目里最值得记住的结论

1. 实现 Serializable 只是开始,不是全部

真正难点在于:

  • 版本兼容
  • 对象图依赖
  • 敏感字段控制
  • 安全风险控制

2. serialVersionUID 最好显式声明

这不是形式问题,而是兼容性治理问题。

3. transient 只影响默认序列化

如果你在 writeObject / readObject 中手动处理,它仍然可以被写出和恢复。

4. 父类不序列化,子类仍可序列化

但父类状态依赖其无参构造恢复,不会自动按原值还原。

5. 生产环境中,Java 原生反序列化必须慎用

尤其是接收外部数据时,绝不能把它当成普通输入处理。


总结

Java 序列化和反序列化并不难入门,真正难的是理解它背后的边界和代价。

如果只是写一个 Demo,实现 Serializable、调用对象流 API 就够了;但一旦进入真实项目,马上会遇到这些问题:

  • 类结构变更后还能不能兼容旧数据
  • 哪些字段应该排除
  • 敏感信息会不会泄漏
  • 反序列化是否安全
  • 是否应该改用 JSON、ProtoBuf、Kryo 等替代方案

因此,正确的学习路径应该是:

先会用,再理解机制,最后知道什么时候不该用。

只有这样,序列化知识才不是停留在语法层面,而是真正能服务于工程实践。

正文到此结束
评论插件初始化中...
Loading...