序列流(序列化和反序列化)

序列流(序列化和反序列化)

序列化

Java 序列化流的核心作用是将内存中的Java对象转换为字节序列(序列化),或将字节序列还原为内存中的Java对象(反序列化),实现对象的持久化存储(如写入文件)或网络传输(如分布式系统间对象传递)。以下是序列化流的完整解析,涵盖核心概念、API使用、规则与注意事项。

一、核心概念

1. 序列化(Serialization)

将Java对象(包含属性、状态信息)转换为二进制字节流的过程,目的是脱离JVM内存环境,实现对象的“异地迁移”或“持久化保存”。

2. 反序列化(Deserialization)

将序列化生成的字节流还原为原始Java对象的过程,还原后的对象与原对象状态一致(属性值相同,独立于原对象)。

3. 序列化的核心前提

一个类要能被序列化,必须实现 java.io.Serializable​ 接口(标记接口,无任何抽象方法,仅用于告知JVM该类支持序列化),否则序列化时会抛出 NotSerializableException​。

二、核心序列化/反序列化流 API

Java IO 提供了专门用于对象序列化的字节流,核心是 ObjectOutputStream​(序列化流)和 ObjectInputStream​(反序列化流),均继承自字节流父类,依赖字节流实现底层存储/传输。

1. 序列化流:ObjectOutputStream​

核心作用

将Java对象写入字节输出流(可关联文件、网络套接字等),生成序列化字节序列。

关键API

方法

作用

​ObjectOutputStream(OutputStream out)​

构造方法:传入底层字节输出流(如 FileOutputStream​),包装为对象序列化流

​void writeObject(Object obj)​

核心方法:将指定对象序列化并写入输出流

​void close()​

关闭流,释放资源

使用示例(对象写入文件)

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

// 待序列化的类:必须实现 Serializable 接口
class User implements Serializable {
    // 序列化版本号:用于反序列化时校验类结构是否一致
    private static final long serialVersionUID = 1L;
    
    private String username;
    private int age;
    // 瞬态字段:不会被序列化(transient 修饰)
    private transient String password;

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

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

public class SerializationDemo {
    public static void main(String[] args) {
        // 1. 创建待序列化对象
        User user = new User("zhangsan", 25, "123456");

        // 2. 构建 ObjectOutputStream(依赖 FileOutputStream 写入文件)
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
            // 3. 序列化对象并写入文件
            oos.writeObject(user);
            System.out.println("对象序列化成功,已写入 user.ser 文件");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2. 反序列化流:ObjectInputStream​

核心作用

从字节输入流中读取序列化字节序列,还原为Java对象。

关键API

方法

作用

​ObjectInputStream(InputStream in)​

构造方法:传入底层字节输入流(如 FileInputStream​),包装为对象反序列化流

​Object readObject()​

核心方法:读取字节序列并反序列化为对象,返回值为 Object​,需强制类型转换

​void close()​

关闭流,释放资源

使用示例(从文件读取并反序列化对象)

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class DeserializationDemo {
    public static void main(String[] args) {
        // 1. 构建 ObjectInputStream(依赖 FileInputStream 读取文件)
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
            // 2. 反序列化对象:强制类型转换为 User 类型
            User user = (User) ois.readObject();
            System.out.println("对象反序列化成功:" + user);
            // 注意:transient 字段 password 未被序列化,反序列化后为 null
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果:对象反序列化成功:User{username='zhangsan', age=25, password='null'}​(password​ 为 transient​,未被序列化)

三、序列化核心规则与特性

1. 关键字 transient​:排除序列化字段

  • 作用:被 transient​ 修饰的字段,序列化时会被忽略,反序列化后该字段值为默认值(引用类型为 null​,基本类型为 0/false​ 等);

  • 适用场景:无需持久化/传输的临时字段(如缓存数据、敏感信息(密码))。

2. 序列化版本号 serialVersionUID​

  • 作用:serialVersionUID 被称为序列化 ID,它是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,Java 虚拟机会把字节流中的 serialVersionUID 与被序列化类中的 serialVersionUID 进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。

  • 添加方式:

    1)添加一个默认版本的序列化 ID:

    private static final long serialVersionUID = 1L。
    

    2)添加一个随机生成的不重复的序列化 ID。

    private static final long serialVersionUID = -2095916884810199532L;
    

    3)添加 @SuppressWarnings​ 注解。

    @SuppressWarnings("serial")
    

核心作用

用于反序列化时校验“序列化时的类结构”与“反序列化时的类结构”是否一致,避免类结构变更导致反序列化失败。

生成与使用

  • 手动指定:推荐在序列化类中显式声明 private static final long serialVersionUID = 固定值;​(如示例中的 1L​);

  • 自动生成:IDE(IDEA/Eclipse)可自动生成唯一的序列化版本号(基于类名、字段、方法等计算);

  • 未指定的风险:若类结构变更(如新增/删除字段),JVM会重新计算版本号,导致反序列化时抛出 InvalidClassException​(版本号不匹配);

  • 指定后的优势:即使类结构小幅变更(如新增非必需字段),只要版本号不变,反序列化仍可成功(新增字段取默认值,删除字段忽略)。

3. static​ 字段不参与序列化

​static​ 字段属于类级别的变量,不属于单个实例对象的状态,因此序列化仅针对实例字段,static​ 字段不会被序列化/反序列化(反序列化后读取的是当前类的 static​ 字段值,而非序列化时的旧值)。

4. 父类序列化规则

  • 若父类实现了 Serializable​ 接口:子类序列化时,会自动序列化父类的所有非 transient​、非 static​ 实例字段;

  • 若父类未实现 Serializable​ 接口:父类必须提供无参构造方法,否则子类反序列化时会抛出 InvalidClassException​;此时子类仅序列化自身的字段,父类字段通过无参构造方法初始化(取默认值)。

5. 集合/数组的序列化

  • Java 内置集合(ArrayList​、HashMap​ 等)均已实现 Serializable​ 接口,可直接序列化(需保证集合中的元素也实现 Serializable​);

  • 数组(基本类型数组、引用类型数组)均可序列化(引用类型数组需保证数组元素实现 Serializable​)。

示例(序列化 ArrayList​ 集合):

try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("userList.ser"))) {
    List<User> userList = new ArrayList<>();
    userList.add(new User("zhangsan", 25, "123456"));
    userList.add(new User("lisi", 28, "654321"));
    oos.writeObject(userList); // 集合序列化
} catch (Exception e) {
    e.printStackTrace();
}

四、序列化的常见场景

  1. 对象持久化:将对象写入本地文件、数据库BLOB字段,实现对象状态的持久化存储(如用户会话持久化、配置对象保存);

  2. 网络传输:分布式系统(如Dubbo、RMI)中,通过网络传输对象时,需先将对象序列化为字节流,传输后再反序列化为对象;

  3. 缓存存储:将复杂对象存入缓存(如Redis)时,需先序列化为字节流或JSON(序列化的一种轻量形式),读取时反序列化。

五、注意事项与坑点

  1. 敏感信息泄露:序列化会将对象的所有非 transient​ 字段写入字节流,若包含密码、令牌等敏感信息,可能导致泄露,需用 transient​ 修饰或加密存储;

  2. 类结构变更风险:未指定 serialVersionUID​ 时,类结构变更会导致反序列化失败,建议所有序列化类显式声明版本号;

  3. 流关闭顺序:使用 ObjectOutputStream​/ObjectInputStream​ 时,需保证流正常关闭(推荐使用 try-with-resources 语法,自动关闭流);

  4. 反序列化安全:恶意构造的序列化字节流可能导致安全漏洞(如远程代码执行),JDK 提供了 ObjectInputFilter​ 用于过滤非法反序列化对象,生产环境需开启安全校验。

六、总结

  1. 核心流:ObjectOutputStream​(序列化,写对象)、ObjectInputStream​(反序列化,读对象),依赖字节流实现底层IO;

  2. 核心前提:待序列化类必须实现 Serializable​ 接口,推荐显式声明 serialVersionUID​;

  3. 字段控制:transient​ 排除序列化字段,static​ 字段不参与序列化;

  4. 核心规则:父类序列化需实现接口或提供无参构造,集合/数组可直接序列化(元素需满足序列化要求);

  5. 核心场景:对象持久化、网络传输、缓存存储,是Java分布式系统和持久化方案的基础。

IO流详解:如何轻松理解输入流和输出流 2026-07-13