10Effective Java(第3版)

序列化

序列化最佳实践

serialVersionUIDtransient序列化代理readObject

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第10章:序列化(Serialization)

一、导读

1.1 本章学习目标

第10章探讨 Java 序列化系统的最佳实践。序列化是将对象转换为字节流的过程,反序列化则是将字节流恢复为对象的过程。本章通过5条经验法则,帮助你正确使用序列化,避免常见的陷阱。

本章的核心学习目标包括:

  • 理解序列化的风险:掌握序列化带来的安全和兼容性问题。
  • 学会实现 Serializable 的正确方式:掌握 serialVersionUID 的作用。
  • 掌握 readObject 的防御性编程:保护对象的不变量。
  • 理解序列化代理模式:用序列化代理替代默认的序列化形式。
  • 了解序列化的替代方案:掌握 JSON、Protocol Buffers 等替代方案。

1.2 为什么第十章如此重要

序列化看似简单,实则暗藏危机。不正确的序列化实现会导致安全漏洞、版本兼容性问题、性能问题。随着分布式系统和微服务架构的普及,序列化的重要性愈发凸显。

1.3 核心问题引导

为什么实现 Serializable 需要慎重考虑?

serialVersionUID 的作用是什么?不指定会怎样?

如何在反序列化时保护对象的不变量?

什么是序列化代理模式?它解决了什么问题?

有哪些替代 Java 原生序列化的方案?


二、核心概念详解

10.1 谨慎地实现 Serializable(Item 85)

10.1.1 实现 Serializable 的代价

降低灵活性:一旦发布,类的序列化形式就成为 API 的一部分。

增加测试负担:需要确保序列化和反序列化的正确性。

增加安全攻击面:反序列化可能执行恶意代码。

版本兼容问题:类的变化可能导致旧版本的序列化数据无法反序列化。

java
// 如果不想被序列化,不要实现 Serializable
public class UserService {
    // 服务类通常不需要序列化
}

// 如果必须实现,考虑使用序列化代理
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
}

10.1.2 serialVersionUID 的作用

java
// 不指定 serialVersionUID:编译器自动生成
// 类的任何变化都可能导致 serialVersionUID 变化
// 旧版本的序列化数据无法反序列化

// 指定 serialVersionUID:保持版本兼容
public class User implements Serializable {
    private static final long serialVersionUID = 42L;
    private String name;
    private int age;
    
    // 新增字段不影响旧版本的反序列化
    private String email;  // 旧版本数据中 email 为 null
}

10.2 考虑使用序列化代理(Item 90)

10.2.1 序列化代理模式

java
public final class Period implements Serializable {
    private final Date start;
    private final Date end;

    // 序列化代理
    private static class SerializationProxy implements Serializable {
        private static final long serialVersionUID = 1L;
        private final Date start;
        private final Date end;

        SerializationProxy(Period p) {
            this.start = new Date(p.start.getTime());
            this.end = new Date(p.end.getTime());
        }

        private Object readResolve() {
            return new Period(start, end);  // 调用公共构造器
        }
    }

    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }
}

序列化代理的优势:

安全性:通过 readResolve 保证不变量。

灵活性:可以改变序列化形式而不影响反序列化。

类型安全:反序列化的对象类型由代理控制。

10.3 考虑用替代方案(Item 90 补充)

Java 原生序列化有很多问题,现代应用通常使用替代方案:

方案优点缺点
JSON可读性好,跨语言不支持复杂类型
Protocol Buffers高效,跨语言,有 schema需要定义 proto 文件
Avro高效,支持 schema 演化需要 schema 注册表
Kryo高效,Java 专用不支持跨语言
java
// JSON 序列化(Jackson)
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user);
User user = mapper.readValue(json, User.class);

// Protocol Buffers
// 定义 user.proto
// message User {
//     string name = 1;
//     int32 age = 2;
// }
UserProto.User user = UserProto.User.newBuilder()
    .setName("Alice")
    .setAge(30)
    .build();
byte[] bytes = user.toByteArray();
UserProto.User parsed = UserProto.User.parseFrom(bytes);

三、实战案例

10.1 安全的序列化实现

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

    private final String name;
    private final int age;
    private final transient String password;  // 不序列化

    public User(String name, int age, String password) {
        this.name = Objects.requireNonNull(name);
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Invalid age: " + age);
        }
        this.age = age;
        this.password = Objects.requireNonNull(password);
    }

    // 序列化代理
    private static class SerializationProxy implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name;
        private int age;

        SerializationProxy(User user) {
            this.name = user.name;
            this.age = user.age;
        }

        private Object readResolve() {
            return new User(name, age, "default");
        }
    }

    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }

    // getter
    public String getName() { return name; }
    public int getAge() { return age; }
}

10.2 JSON 序列化最佳实践

java
public class JsonSerialization {
    private static final ObjectMapper MAPPER = new ObjectMapper()
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        .setSerializationInclusion(JsonInclude.Include.NON_NULL)
        .registerModule(new JavaTimeModule())
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

    // 序列化
    public static String toJson(Object obj) {
        try {
            return MAPPER.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("JSON serialization failed", e);
        }
    }

    // 反序列化
    public static <T> T fromJson(String json, Class<T> type) {
        try {
            return MAPPER.readValue(json, type);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("JSON deserialization failed", e);
        }
    }

    // 类型安全的反序列化
    public static <T> T fromJson(String json, TypeReference<T> typeRef) {
        try {
            return MAPPER.readValue(json, typeRef);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("JSON deserialization failed", e);
        }
    }
}

// 使用
User user = new User("Alice", 30, "secret");
String json = JsonSerialization.toJson(user);
User parsed = JsonSerialization.fromJson(json, User.class);

四、本章小结

第10章系统讲解了序列化的核心知识:

谨慎实现 Serializable:序列化有安全、兼容性和性能代价。

使用 serialVersionUID:保持版本兼容性。

使用序列化代理:保证反序列化的安全性。

考虑替代方案:JSON、Protocol Buffers 等通常优于 Java 原生序列化。

transient 字段:敏感数据不应该被序列化。

序列化是 Java 中最容易出问题的特性之一。记住:如果可以避免,就不要使用 Java 原生序列化。


五、思考与练习

思考题

Java 原生序列化有哪些安全问题?

readResolve 方法的作用是什么?

为什么 transient 字段不会被序列化?

JSON 和 Protocol Buffers 各有什么优缺点?

如何保证序列化数据的版本兼容性?

编程练习

实现安全的序列化:使用序列化代理模式实现一个不可变类的序列化。

JSON 序列化:使用 Jackson 实现对象的 JSON 序列化和反序列化。

版本兼容:设计一个支持版本演化的序列化方案。


10/10序列化
0%