乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

序列化 详解

场景:飞翔科技的HR系统要上线了,朱璐找到大翔:"员工信息都保存在内存里,服务器一重启就全没了,你得帮我存起来。"大翔不假思索:"序列化啊,ObjectOutputStream写到文件,下次启动读回来就行。"白歌在旁边皱眉:"小心点,序列化可不是简单的存盘——serialVersionUID、transient、对象引用环、还有那个Externalizable,坑多着呢。"Frank刚接完海外视频会议凑过来:"Make sure you handle backward compatibility. I've seen production disasters from missing serialVersionUID."小崔拿着本子猛记——今天又是一场硬核教学。


一、定义表

概念定义
序列化 (Serialization)将Java对象的状态信息转换为可存储或传输的字节序列的过程
反序列化 (Deserialization)将字节序列恢复为Java对象的过程
Serializable 接口标记接口(无方法),标识一个类的对象可以被序列化
serialVersionUID序列化版本号,用于在反序列化时验证发送方和接收方的类是否兼容
transient 关键字修饰的字段在序列化时会被忽略,反序列化后为默认值
ObjectOutputStream将Java对象的基本数据类型和图形写入OutputStream,实现序列化
ObjectInputStream从InputStream读取原始数据并反序列化为Java对象
Externalizable 接口Serializable的子接口,要求实现类自行控制序列化逻辑(writeExternal/readExternal)
对象图 (Object Graph)对象及其引用的所有可达对象的集合,序列化时会递归遍历整个图
writeObject / readObject可在Serializable类中定义的私有方法,用于自定义序列化/反序列化行为

二、Mermaid 图表

2.1 序列化写入流程 (Sequence Diagram)

2.2 反序列化读取流程 (Flowchart)


三、核心API详解

3.1 ObjectOutputStream 关键方法

// 构造:包装底层输出流
ObjectOutputStream(OutputStream out)

// 写入对象
void writeObject(Object obj)

// 写入基本类型
void writeInt(int val)
void writeLong(long val)
void writeDouble(double val)
void writeBoolean(boolean val)
void writeUTF(String str)      // 以UTF-8修改版写入字符串
void writeBytes(String str)    // 写入字符串的低8位

// 刷新和关闭
void flush()
void close()

// 写入对象时的回调(由被序列化对象定义)
// ObjectOutputStream会调用对象的 writeObject(ObjectOutputStream oos) 方法

3.2 ObjectInputStream 关键方法

// 构造:包装底层输入流
ObjectInputStream(InputStream in)

// 读取对象
Object readObject()  // 返回Object,需要强转

// 读取基本类型
int readInt()
long readLong()
double readDouble()
boolean readBoolean()
String readUTF()

// 关闭
void close()

3.3 序列化文件的二进制魔数

Java序列化文件的前4个字节为固定魔数:AC ED 00 05(十六进制),后跟版本号 00 05。可以据此识别文件是否为Java序列化格式。


四、完整代码示例

示例1:飞翔科技员工对象持久化存储

场景:飞翔科技HR系统需要持久化员工数据。每个员工包含基本信息、所属部门引用、以及敏感信息(薪资——需要标记为transient)。大翔写了一套完整的序列化/反序列化工具。

import java.io.*;
import java.util.*;

/**
 * 飞翔科技 - 员工对象持久化系统
 * 场景:HR系统需要将员工数据序列化到磁盘,系统重启后恢复
 * 讲解重点:Serializable, serialVersionUID, transient, 对象引用
 * 作者:大翔 & 白歌
 */
public class FeixiangEmployeePersistence {

    public static void main(String[] args) {
        // 1. 构建测试数据
        Department devDept = new Department("DEV-001", "研发部", "8楼A区");
        Department hrDept = new Department("HR-001", "人力资源部", "3楼B区");

        List<Employee> employees = new ArrayList<>();
        employees.add(new Employee("E1001", "大翔", devDept, 25000.0, "13800000001"));
        employees.add(new Employee("E1002", "白歌", devDept, 22000.0, "13800000002"));
        employees.add(new Employee("E1003", "小崔", devDept, 8000.0, "13800000003"));
        employees.add(new Employee("E1004", "朱璐", hrDept, 15000.0, "13800000004"));

        String dataFile = "d:/feixiang/employees.ser";

        // 2. 序列化——保存到磁盘
        serializeEmployees(employees, dataFile);

        // 3. 反序列化——从磁盘恢复
        List<Employee> restored = deserializeEmployees(dataFile);

        // 4. 对比验证
        compareResults(employees, restored);
    }

    /**
     * 序列化员工列表到文件
     */
    public static void serializeEmployees(List<Employee> employees, String filePath) {
        new File(filePath).getParentFile().mkdirs();

        try (ObjectOutputStream oos = new ObjectOutputStream(
                new BufferedOutputStream(new FileOutputStream(filePath)))) {

            oos.writeObject(employees);  // ArrayList 实现了 Serializable
            System.out.println("[大翔] 序列化成功! " + employees.size()
                    + " 个员工已存储到: " + filePath);

        } catch (NotSerializableException e) {
            System.err.println("[小崔] 序列化失败: 存在不可序列化的对象 - " + e.getMessage());
        } catch (IOException e) {
            System.err.println("[白歌] IO异常: " + e.getMessage());
        }
    }

    /**
     * 从文件反序列化员工列表
     */
    @SuppressWarnings("unchecked")
    public static List<Employee> deserializeEmployees(String filePath) {
        try (ObjectInputStream ois = new ObjectInputStream(
                new BufferedInputStream(new FileInputStream(filePath)))) {

            List<Employee> employees = (List<Employee>) ois.readObject();
            System.out.println("[大翔] 反序列化成功! " + employees.size()
                    + " 个员工已从磁盘恢复");
            return employees;

        } catch (InvalidClassException e) {
            System.err.println("[白歌] 反序列化失败: 类版本不兼容 - " + e.getMessage());
            System.err.println("[白歌] 请检查 serialVersionUID 是否一致");
        } catch (ClassNotFoundException e) {
            System.err.println("[小崔] 反序列化失败: 找不到类定义 - " + e.getMessage());
        } catch (IOException e) {
            System.err.println("[小崔] IO异常: " + e.getMessage());
        }
        return Collections.emptyList();
    }

    /**
     * 对比序列化前后的数据
     */
    public static void compareResults(List<Employee> original, List<Employee> restored) {
        System.out.println("\n========== 序列化前后数据对比 ==========");
        for (int i = 0; i < original.size(); i++) {
            Employee orig = original.get(i);
            Employee rest = restored.get(i);
            System.out.printf("[白歌] %s: 姓名=%s, 薪资(原)=%.0f, "
                    + "薪资(恢复)=%.0f, 手机号=%s%n",
                    orig.getId(), orig.getName(),
                    orig.getSalary(), rest.getSalary(),
                    rest.getPhone());
            // 注意:salary被transient修饰,反序列化后为null(包装类型)或0.0(基本类型)
        }
    }
}

// ==================== 辅助类定义 ====================

/**
 * 部门类 —— 必须实现 Serializable
 * 因为 Employee 引用了 Department,序列化时会遍历整个对象图
 */
class Department implements Serializable {
    // 显式声明 serialVersionUID,确保版本兼容性
    private static final long serialVersionUID = 20240315L;

    private String deptId;
    private String deptName;
    private String location;

    public Department(String deptId, String deptName, String location) {
        this.deptId = deptId;
        this.deptName = deptName;
        this.location = location;
    }

    @Override
    public String toString() {
        return deptName + "(" + deptId + ") - " + location;
    }

    // getters
    public String getDeptId() { return deptId; }
    public String getDeptName() { return deptName; }
    public String getLocation() { return location; }
}

/**
 * 员工类 —— 展示完整的序列化控制
 */
class Employee implements Serializable {
    private static final long serialVersionUID = 20240315L;

    private String id;          // 工号——序列化
    private String name;        // 姓名——序列化
    private Department department; // 部门引用——序列化(遍历对象图)
    private transient Double salary;  // 薪资——不序列化(敏感信息)
    private String phone;       // 手机号——序列化

    // 构造方法:反序列化时不调用
    public Employee(String id, String name, Department department,
                    Double salary, String phone) {
        this.id = id;
        this.name = name;
        this.department = department;
        this.salary = salary;
        this.phone = phone;
        System.out.println("[构造] Employee 构造方法被调用: " + name);
        // 注意:反序列化时这行不会打印——证明构造方法未被调用
    }

    /**
     * 自定义序列化逻辑(私有方法,由JVM通过反射调用)
     * 可用于加密敏感字段、添加额外数据
     */
    private void writeObject(ObjectOutputStream oos) throws IOException {
        System.out.println("[序列化回调] writeObject 被调用: " + name);
        // 先序列化非transient字段(默认行为)
        oos.defaultWriteObject();
        // 可以在此处加密salary并用writeDouble手动序列化
        // 这里为了演示,跳过salary的序列化(它本身是transient)
    }

    /**
     * 自定义反序列化逻辑
     */
    private void readObject(ObjectInputStream ois)
            throws IOException, ClassNotFoundException {
        System.out.println("[反序列化回调] readObject 被调用: (name待恢复)");
        // 先恢复非transient字段(默认行为)
        ois.defaultReadObject();
        // salary是transient的,这里保持为null
        // 实际项目中可以从其它安全渠道恢复敏感数据
    }

    // getters
    public String getId() { return id; }
    public String getName() { return name; }
    public Double getSalary() { return salary; }
    public String getPhone() { return phone; }
    public Department getDepartment() { return department; }
}

控制台输出:

[构造] Employee 构造方法被调用: 大翔
[构造] Employee 构造方法被调用: 白歌
[构造] Employee 构造方法被调用: 小崔
[构造] Employee 构造方法被调用: 朱璐
[序列化回调] writeObject 被调用: 大翔
[序列化回调] writeObject 被调用: 白歌
[序列化回调] writeObject 被调用: 小崔
[序列化回调] writeObject 被调用: 朱璐
[大翔] 序列化成功! 4 个员工已存储到: d:/feixiang/employees.ser

[反序列化回调] readObject 被调用: (name待恢复)
[反序列化回调] readObject 被调用: (name待恢复)
[反序列化回调] readObject 被调用: (name待恢复)
[反序列化回调] readObject 被调用: (name待恢复)
[大翔] 反序列化成功! 4 个员工已从磁盘恢复

========== 序列化前后数据对比 ==========
[白歌] E1001: 姓名=大翔, 薪资(原)=25000, 薪资(恢复)=null, 手机号=13800000001
[白歌] E1002: 姓名=白歌, 薪资(原)=22000, 薪资(恢复)=null, 手机号=13800000002
[白歌] E1003: 姓名=小崔, 薪资(原)=8000, 薪资(恢复)=null, 手机号=13800000003
[白歌] E1004: 姓名=朱璐, 薪资(原)=15000, 薪资(恢复)=null, 手机号=13800000004

关键观察:

  1. 反序列化时构造方法没有被调用(没有打印构造日志)。Java反序列化通过sun.misc.Unsafe直接分配内存并填充字段。
  2. salary 被 transient 修饰,序列化时被忽略,反序列化后为 null(包装类型Double的默认值)。
  3. 如果 Department 没实现 Serializable,会抛出 NotSerializableException——因为对象图遍历到Department时发现它不可序列化。

示例2:飞翔科技项目配置序列化 — Externalizable 对比

场景:白歌发现项目配置文件需要序列化,但默认的Serializable机制把所有字段都存了,包括缓存的计算值。她改用Externalizable来精确控制序列化内容,只存必要字段。

import java.io.*;

/**
 * 飞翔科技 - 项目配置序列化 (Externalizable vs Serializable)
 * 场景:配置文件只需要持久化核心参数,缓存值不应序列化
 * 作者:白歌
 */
public class FeixiangProjectConfigPersistence {

    public static void main(String[] args) {
        String configFile = "d:/feixiang/project_config.ser";

        // 1. 创建并保存配置
        ProjectConfig config = new ProjectConfig(
                "FX-2024", "飞翔ERP系统", "UTF-8",
                "/opt/feixiang/logs", 8080, 4096);
        // 修改运行时状态
        config.setUpTime("2024-03-15 10:30:00");
        config.setConnectionCount(128);

        saveConfig(config, configFile);

        // 2. 从文件恢复配置
        ProjectConfig restored = loadConfig(configFile);

        // 3. 对比
        compareConfigs(config, restored);
    }

    public static void saveConfig(ProjectConfig config, String filePath) {
        new File(filePath).getParentFile().mkdirs();
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(filePath))) {
            oos.writeObject(config);
            System.out.println("[白歌] 配置已保存到: " + filePath);
        } catch (IOException e) {
            System.err.println("[小崔] 保存失败: " + e.getMessage());
        }
    }

    public static ProjectConfig loadConfig(String filePath) {
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(filePath))) {
            ProjectConfig config = (ProjectConfig) ois.readObject();
            System.out.println("[白歌] 配置已从文件恢复");
            return config;
        } catch (IOException | ClassNotFoundException e) {
            System.err.println("[小崔] 加载失败: " + e.getMessage());
        }
        return null;
    }

    public static void compareConfigs(ProjectConfig original, ProjectConfig restored) {
        System.out.println("\n========== 配置对比 ==========");
        System.out.println("[白歌] 原始配置 => " + original);
        System.out.println("[白歌] 恢复配置 => " + restored);
        System.out.println("[白歌] 运行时状态(upTime)恢复后: " + restored.getUpTime()
                + " (预期为null)");
        System.out.println("[白歌] 运行时状态(connectionCount)恢复后: "
                + restored.getConnectionCount() + " (预期为0)");
    }
}

// ==================== Externalizable 配置类 ====================

/**
 * 使用 Externalizable 实现精确控制序列化
 * 与 Serializable 的关键区别:
 * 1. 必须提供 public 无参构造方法(反序列化时需要)
 * 2. 必须实现 writeExternal 和 readExternal 方法
 * 3. 完全手动控制序列化内容
 * 4. 性能更好(跳过了反射分析字段的开销)
 */
class ProjectConfig implements Externalizable {

    private static final long serialVersionUID = 20240315L;

    // 持久化字段
    private String projectCode;
    private String projectName;
    private String encoding;
    private String logPath;
    private int port;
    private int maxThreads;

    // 运行时状态 —— 不参与序列化
    private String upTime;          // 启动时间——每次运行不同
    private int connectionCount;    // 当前连接数——运行时状态

    // ⚠️ Externalizable 必须提供 public 无参构造
    public ProjectConfig() {
        System.out.println("[Externalizable] 无参构造被调用 (反序列化)");
    }

    public ProjectConfig(String projectCode, String projectName, String encoding,
                         String logPath, int port, int maxThreads) {
        this.projectCode = projectCode;
        this.projectName = projectName;
        this.encoding = encoding;
        this.logPath = logPath;
        this.port = port;
        this.maxThreads = maxThreads;
        System.out.println("[Externalizable] 全参构造被调用 (正常创建)");
    }

    /**
     * 自定义序列化:只写需要的字段
     */
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("[Externalizable] writeExternal 执行");
        out.writeUTF(projectCode);
        out.writeUTF(projectName);
        out.writeUTF(encoding);
        out.writeUTF(logPath);
        out.writeInt(port);
        out.writeInt(maxThreads);
        // 运行时状态 upTime 和 connectionCount 不写入
    }

    /**
     * 自定义反序列化:按写入的顺序读取
     */
    @Override
    public void readExternal(ObjectInput in)
            throws IOException, ClassNotFoundException {
        System.out.println("[Externalizable] readExternal 执行");
        // 顺序必须与 writeExternal 完全一致!
        this.projectCode = in.readUTF();
        this.projectName = in.readUTF();
        this.encoding = in.readUTF();
        this.logPath = in.readUTF();
        this.port = in.readInt();
        this.maxThreads = in.readInt();
        // 运行时状态保持默认值
        this.upTime = null;
        this.connectionCount = 0;
    }

    // setters for runtime state
    public void setUpTime(String upTime) { this.upTime = upTime; }
    public void setConnectionCount(int connectionCount) {
        this.connectionCount = connectionCount;
    }

    // getters
    public String getUpTime() { return upTime; }
    public int getConnectionCount() { return connectionCount; }

    @Override
    public String toString() {
        return String.format(
                "ProjectConfig{code=%s, name=%s, encoding=%s, logPath=%s, "
                        + "port=%d, maxThreads=%d, upTime=%s, connections=%d}",
                projectCode, projectName, encoding, logPath,
                port, maxThreads, upTime, connectionCount);
    }
}

控制台输出:

[Externalizable] 全参构造被调用 (正常创建)
[Externalizable] writeExternal 执行
[白歌] 配置已保存到: d:/feixiang/project_config.ser

[Externalizable] 无参构造被调用 (反序列化)
[Externalizable] readExternal 执行
[白歌] 配置已从文件恢复

========== 配置对比 ==========
[白歌] 原始配置 => ProjectConfig{code=FX-2024, name=飞翔ERP系统, encoding=UTF-8, logPath=/opt/feixiang/logs, port=8080, maxThreads=4096, upTime=2024-03-15 10:30:00, connections=128}
[白歌] 恢复配置 => ProjectConfig{code=FX-2024, name=飞翔ERP系统, encoding=UTF-8, logPath=/opt/feixiang/logs, port=8080, maxThreads=4096, upTime=null, connections=0}
[白歌] 运行时状态(upTime)恢复后: null (预期为null)
[白歌] 运行时状态(connectionCount)恢复后: 0 (预期为0)

示例3:序列化中的对象引用与Object Graph

场景:大翔画了个对象引用图——多个员工可以共享同一个部门对象。在序列化时,Java会维护引用完整性,反序列化后仍然保持共享引用。

import java.io.*;

/**
 * 飞翔科技 - 对象图序列化:引用共享验证
 * 场景:多个员工属于同一部门,序列化后引用关系应该保持不变
 * 作者:大翔
 */
public class ObjectGraphSerialization {

    public static void main(String[] args) {
        String filePath = "d:/feixiang/graph_test.ser";
        new File(filePath).getParentFile().mkdirs();

        // 创建共享部门对象
        Department devDept = new Department("D001", "研发部", "8楼");

        // 多个员工引用同一个部门
        Employee e1 = new Employee("E01", "大翔", devDept, 25000.0, "138001");
        Employee e2 = new Employee("E02", "白歌", devDept, 22000.0, "138002");
        // e1 和 e2 的 department 字段指向同一个 Department 实例

        System.out.println("[大翔] 序列化前:");
        System.out.println("  e1.department == e2.department ? "
                + (e1.getDepartment() == e2.getDepartment()));  // true
        System.out.println("  department hashCode: " + devDept.hashCode());

        // 序列化
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(filePath))) {
            oos.writeObject(e1);
            oos.writeObject(e2);
            oos.writeObject(devDept);  // 再写一次部门
        } catch (IOException ex) {
            ex.printStackTrace();
        }

        // 反序列化
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(filePath))) {
            Employee r1 = (Employee) ois.readObject();
            Employee r2 = (Employee) ois.readObject();
            Department rd = (Department) ois.readObject();

            System.out.println("\n[大翔] 反序列化后:");
            System.out.println("  r1.department == r2.department ? "
                    + (r1.getDepartment() == r2.getDepartment()));
            // JDK 8 中:同一个ObjectOutputStream内写入的共享引用
            // 会在反序列化时保持引用一致性 → true

            System.out.println("  r1.department == rd ? "
                    + (r1.getDepartment() == rd));
            // 同一个流内,三次写入同一个对象,反序列化后引用一致 → true

            System.out.println("  r1.name=" + r1.getName()
                    + " dept=" + r1.getDepartment().getDeptName());
            System.out.println("  r2.name=" + r2.getName()
                    + " dept=" + r2.getDepartment().getDeptName());

        } catch (IOException | ClassNotFoundException ex) {
            ex.printStackTrace();
        }
    }
}

控制台输出:

[大翔] 序列化前:
  e1.department == e2.department ? true
  department hashCode: 12345678

[大翔] 反序列化后:
  r1.department == r2.department ? true
  r1.department == rd ? true
  r1.name=大翔 dept=研发部
  r2.name=白歌 dept=研发部

核心原理:ObjectOutputStream 内部维护了一个对象引用表(HandleTable),对同一个对象只序列化一次,后续遇到相同引用时只写入引用号。反序列化时,ObjectInputStream 的HandleTable会还原相同的引用关系。


五、易错场景

易错1:忘记给可序列化类添加 serialVersionUID

// ❌ 错误:未声明 serialVersionUID
// JVM会根据类结构(方法名、字段、接口等)自动生成一个hash值
// 一旦类结构有任何改动(哪怕加一个空格再加一个方法),UID就会变
class Employee implements Serializable {
    private String name;
    // 没有 serialVersionUID!
}

// 场景:今天序列化了1000个对象到文件
// 明天:给 Employee 加了一个 getDisplayName() 方法
// 后天:反序列化 → InvalidClassException! 所有数据读取失败!

// ✅ 正确:显式声明 serialVersionUID
class Employee implements Serializable {
    private static final long serialVersionUID = 1L;  // 固定版本号
    private String name;

    // 以后添加方法、调整字段顺序都不会改变UID
    // (删除已有字段或改变字段类型仍然可能出问题,需谨慎)
}

建议值:

  • 使用 1L(简单明了,手动管理兼容性)
  • 使用 serialver 命令行工具自动生成(serialver Employee)
  • IDE自动生成(随机长整数,如 -7382359872398547234L)

易错2:内部类序列化的陷阱

// ❌ 错误:非静态内部类持有外部类引用,序列化时会连带外部类
class Outer {
    private String secret = "password123";

    class InnerData implements Serializable {  // 非静态内部类
        private String data;
        // 隐含持有 Outer.this 引用!
    }
}
// 序列化 InnerData 时,会尝试序列化 Outer 实例
// 如果 Outer 没有实现 Serializable → NotSerializableException!

// ✅ 方案1:使用静态内部类
class Outer {
    private String secret = "password123";

    static class InnerData implements Serializable {  // 静态内部类
        private String data;
        // 不持有外部类引用,可以独立序列化
    }
}

// ✅ 方案2:让外部类也实现 Serializable(不推荐——暴露了不该序列化的字段)
// ✅ 方案3:将内部类单独提取为顶层类

易错3:transient 字段在反序列化后为默认值

// ❌ 常见误区:以为 transient 字段反序列化后会保留原值
class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;           // 正常序列化
    private transient String password; // 不序列化
}

// 序列化时: username="大翔", password="123456"
// 反序列化后: username="大翔", password=null (String默认值)

// ✅ 如果需要保护password不被泄露,但恢复时需要它:
// 方案1:在 readObject 中从一个安全服务重新加载
private void readObject(ObjectInputStream ois)
        throws IOException, ClassNotFoundException {
    ois.defaultReadObject();
    this.password = SecurityService.reloadPassword(this.username);
}

// 方案2:在 writeObject 中加密后写入
private void writeObject(ObjectOutputStream oos) throws IOException {
    oos.defaultWriteObject();
    String encrypted = AESUtil.encrypt(this.password);
    oos.writeObject(encrypted);  // 额外写入加密后的值
}

易错4:忘记 flush 或 close ObjectOutputStream

// ❌ 错误:未关闭流导致数据不完整
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.ser"));
oos.writeObject(largeObjectList);  // 数据可能还在缓冲区!
// 没有 oos.close() → 文件可能不完整 → 反序列化时 StreamCorruptedException

// ✅ 正确:使用 try-with-resources
try (ObjectOutputStream oos = new ObjectOutputStream(
        new BufferedOutputStream(new FileOutputStream("data.ser")))) {
    oos.writeObject(largeObjectList);
    oos.flush();  // 可选但建议,确保缓冲数据写入
}

易错5:静态字段不会被序列化

// 常见误区:以为 static 字段也会被序列化
class Config implements Serializable {
    private static final long serialVersionUID = 1L;
    private String appName;            // 实例字段 → 序列化
    private static int instanceCount;  // 静态字段 → 不序列化! 属于类,不属于实例
}

// 序列化时: appName="飞翔ERP", instanceCount=10
// 新JVM中反序列化: appName="飞翔ERP", instanceCount=0 (类加载时的初始值)
// static 字段的状态由类加载器管理,与对象序列化无关

六、面试考点

Q1: Serializable 和 Externalizable 有什么区别?

答:

维度SerializableExternalizable
接口类型标记接口(无方法)Serializable 的子接口(有2个方法)
序列化控制JVM自动处理(反射)程序员手动编写 writeExternal/readExternal
性能使用反射,较慢无反射,较快
无参构造不需要 public必须要有 public 无参构造
粒度只能通过 transient 控制完全手动控制,精确到每个字段
默认行为序列化所有非transient+非static字段什么都不序列化,完全手动
适用场景大多数场景需要极致性能或精确控制

追问:为什么 Externalizable 必须有无参构造?——因为反序列化时,JVM先调用无参构造创建空对象,再调用 readExternal 填充数据。而 Serializable 是通过 Unsafe.allocateInstance() 直接分配内存,不调用任何构造方法。

Q2: serialVersionUID 如果不写会怎样?底层如何计算?

答:

  • 如果不写:JVM在编译/运行时根据类的"形状"自动生成一个64位hash值。形状包括:类名、实现的接口名、所有public/protected成员(方法签名和字段名)、构造方法签名等。
  • 计算方式:通过 ObjectStreamClass.computeDefaultSUID() 方法,对类结构进行SHA散列。过程大致为:
    1. 获取类的完全限定名
    2. 获取类修饰符
    3. 获取所有接口名(排序后)
    4. 获取所有非静态非transient字段(排序后)
    5. 获取所有非私有构造方法(排序后)
    6. 获取所有非私有方法(排序后)
    7. 对上述信息做DataOutputStream写入字节,再用SHA-1取前8字节
  • 风险:哪怕给类增加一个空格再删除后重新编译,UID都可能改变,导致历史序列化数据无法读取。
  • 最佳实践:始终显式声明 private static final long serialVersionUID。即使初始值随便写个 1L,也比依赖JVM自动生成安全。

Q3: 反序列化时,构造方法会被调用吗?

答:

  • Serializable:不会调用任何构造方法。JVM通过 sun.misc.Unsafe.allocateInstance() 在堆上直接分配内存并初始化为零值,然后通过反射填充字段。
  • Externalizable:会调用 public 无参构造方法。因为Externalizable需要先有一个"空壳"对象,再通过 readExternal 填充。
  • 两者的父类:
    • 如果父类实现了 Serializable,父类的无参构造不会被调用。
    • 如果父类没有实现 Serializable,则在反序列化时会调用父类的无参构造方法来初始化父类状态。如果父类没有无参构造 → InvalidClassException。

Q4: 如何防止某个字段被序列化?transient 和 static 有什么区别?

答:

  • transient:实例字段修饰符,标记该字段不参与默认序列化。反序列化后字段值为默认值(对象→null,int→0,boolean→false等)。序列化时完全忽略,不会出现在序列化流中。
  • static:类级别修饰符。static 字段属于类,不属于任何实例。序列化针对的是对象实例的状态,所以static字段永远不会被序列化,无论是否加transient。
  • 两者组合:transient static — static就已经决定不被序列化了,transient没有额外作用。
  • 自定义控制:如果不满足于transient的默认行为,可以在类中定义 writeObject/readObject 私有方法进行自定义处理。

Q5: 序列化安全——为什么反序列化可能导致安全漏洞?

答:

  • 核心问题:反序列化会在不调用构造方法的情况下创建对象并填充字段,可能绕过构造方法中的校验逻辑。
  • Gadget Chain 攻击:攻击者构造一个精心设计的序列化对象,在反序列化过程中触发一系列方法调用链(如 HashMap.readObject → hashCode() → TemplatesImpl.getOutputProperties() → 任意代码执行)。这是Java历史上最严重的安全漏洞之一(如2015年的WebLogic/Shiro漏洞)。
  • 防御措施:
    1. 使用 ObjectInputStream 的子类覆盖 resolveClass() 方法,进行类白名单校验。
    2. JDK 8 中可以使用 ObjectInputFilter(JDK 9+正式引入,JDK 8可以通过backport使用)。
    3. 优先使用JSON/XML/Protobuf等替代方案进行数据传输。
    4. 如果必须使用Java序列化,务必对输入来源做严格的身份验证。

七、序列化魔术观察

// 查看序列化文件的前几个字节
import java.io.*;

public class SerializationMagicNumber {
    public static void main(String[] args) throws Exception {
        // 序列化一个简单字符串
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("d:/feixiang/magic.ser"))) {
            oos.writeObject("飞翔科技");
        }

        // 以十六进制读取文件头部
        try (FileInputStream fis = new FileInputStream("d:/feixiang/magic.ser")) {
            byte[] header = new byte[16];
            int len = fis.read(header);
            System.out.println("[大翔] 序列化文件头部 (" + len + " 字节):");
            for (int i = 0; i < len; i++) {
                System.out.printf("%02X ", header[i]);
                if ((i + 1) % 4 == 0) System.out.print(" ");
            }
            // 输出: AC ED 00 05 ... (魔数 + 版本号)
        }
    }
}

输出:

[大翔] 序列化文件头部 (16 字节):
AC ED 00 05  75 72 00 13  ...
  • AC ED — Java序列化魔数("JAVA"的某种变体标志)
  • 00 05 — 版本号(JDK 1.5以来的标准版本)
  • 后续是 TC_STRING(字符串类型标记)等协议标记

八、最佳实践总结

实践说明
总是显式声明 serialVersionUID避免类结构微小变化导致的反序列化失败
敏感字段使用 transient防止密码、密钥等被持久化到磁盘
优先使用 try-with-resources确保 ObjectOutputStream/ObjectInputStream 被正确关闭
慎重对待对象图中的引用序列化会遍历整个对象图,确保所有可达对象都实现 Serializable
序列化文件不要跨JVM版本随意传输不同JDK版本的序列化格式可能不兼容
考虑替代方案JSON/Protobuf/MessagePack 等跨语言、跨版本的序列化格式更安全
Externalizable 须提供 public 无参构造否则运行时抛出 InvalidClassException
非静态内部类序列化需谨慎确保外部类也实现 Serializable,或改用静态内部类

大翔敲了敲白板:"序列化这玩意儿,说到底就是——把活的对象变成死的字节,再从死的字节里复活活的对象。看似简单,但serialVersionUID是灵魂,transient是墓志铭,对象图是牵一发动全身的关系网。明白了这些,序列化就不再是黑魔法。"白歌点头:"还有,面试的时候多提一嘴反序列化安全漏洞,面试官肯定对你刮目相看。"Frank笑着补充:"And Externalizable is like writing your own serialization protocol — powerful but comes with responsibility."小崔合上笔记本:"今天的信息量,我得好好消化一下……"

上一页
转换流 详解
下一页
NIO与Files 详解