《Effective Java》学习笔记——第8部分 序列化

文章目录

      • 一、前言:
      • 二、序列化机制最佳实践
        • [1. 了解序列化的工作原理](#1. 了解序列化的工作原理)
        • [2. 显式声明 serialVersionUID](#2. 显式声明 serialVersionUID)
        • [3. 避免序列化不必要的字段](#3. 避免序列化不必要的字段)
        • [4. 控制序列化过程(自定义序列化)](#4. 控制序列化过程(自定义序列化))
        • [5. 避免序列化中的 Singleton问题](#5. 避免序列化中的 Singleton问题)
        • [6. 避免不必要的反序列化](#6. 避免不必要的反序列化)
        • [7. 考虑序列化的性能](#7. 考虑序列化的性能)
        • [8. 选择合适的序列化机制](#8. 选择合适的序列化机制)
      • 三、小结

一、前言:

《Effective Java》第8部分"序列化"深入讨论了 Java 中的序列化机制及其最佳实践。序列化是将对象的状态转换为字节流,以便于存储或通过网络传输。Java 提供了内置的序列化机制,但在使用时需要谨慎,尤其是在处理复杂对象和版本控制时。合理使用序列化能够确保系统的稳定性、兼容性和性能。

二、序列化机制最佳实践

1. 了解序列化的工作原理
  • 原因:序列化是将 Java 对象的状态转换为字节流的过程,而反序列化则是将字节流转换回对象。了解序列化机制的工作原理对于使用和优化序列化至关重要。

  • 最佳实践:

    • 序列化依赖于 Serializable 接口,这个接口没有方法,它仅仅是一个标记接口。实现该接口的类表示该类的对象可以被序列化。
    • 使用 ObjectOutputStreamObjectInputStream 类来完成对象的序列化和反序列化操作。
  • 示例:

    java 复制代码
    // 实现序列化接口
    public class Person implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name;
        private int age;
    
        // 构造器、getter 和 setter
    }
2. 显式声明 serialVersionUID
  • 原因serialVersionUID 是用于版本控制的标识符。它用于确保反序列化时,序列化版本的兼容性。如果反序列化时类的版本不同且没有显式声明 serialVersionUID,可能会导致 InvalidClassException

  • 最佳实践:

    • 显式声明 serialVersionUID,并确保它在类版本发生更改时更新。这样可以防止因为类的变更导致反序列化失败。
    • 通过手动定义 serialVersionUID,避免 JVM 根据类的结构自动生成它。
  • 示例:

    java 复制代码
    public class Person implements Serializable {
        private static final long serialVersionUID = 1L;  // 显式声明 serialVersionUID
        private String name;
        private int age;
    
        // 构造器、getter 和 setter
    }
3. 避免序列化不必要的字段
  • 原因:序列化会将对象的所有字段都写入字节流。如果对象包含大量不必要的字段(如临时计算的字段或非序列化字段),则会浪费空间,并增加序列化和反序列化的开销。

  • 最佳实践:

    • 使用 transient 关键字标记不需要序列化的字段。这些字段在序列化时将被忽略。
    • 确保只序列化那些真正需要持久化的字段。
  • 示例:

    java 复制代码
    public class Person implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name;
        private transient int age;  // 标记为 transient,避免序列化
    }
4. 控制序列化过程(自定义序列化)
  • 原因:默认的序列化行为可能无法满足某些特定需求。通过自定义序列化方法,可以控制序列化和反序列化的行为,如对字段的加密、压缩等操作。

  • 最佳实践:

    • 重写 writeObject()readObject() 方法来自定义序列化和反序列化的行为。
    • 在自定义序列化过程中,要注意处理 ObjectInputStreamObjectOutputStream,确保序列化的一致性和安全性。
  • 示例:

    java 复制代码
    public class Person implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name;
        private transient int age;  // 标记为 transient,避免序列化
    
        private void writeObject(ObjectOutputStream oos) throws IOException {
            oos.defaultWriteObject();
            // 自定义序列化操作
            oos.writeInt(age);  // 手动序列化 age 字段
        }
    
        private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
            ois.defaultReadObject();
            // 自定义反序列化操作
            age = ois.readInt();  // 手动反序列化 age 字段
        }
    }
5. 避免序列化中的 Singleton问题
  • 原因:序列化和反序列化可能会破坏单例模式,因为反序列化过程中会创建一个新的实例,从而导致多个实例。为了避免这种情况,需要采取特殊措施。

  • 最佳实践:

    • Singleton 类中使用 readResolve() 方法来确保反序列化时返回现有的单例实例。
  • 示例:

    java 复制代码
    public class Singleton implements Serializable {
        private static final long serialVersionUID = 1L;
        private static final Singleton INSTANCE = new Singleton();
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            return INSTANCE;
        }
    
        // 反序列化时返回现有的单例实例
        private Object readResolve() {
            return INSTANCE;
        }
    }
6. 避免不必要的反序列化
  • 原因:反序列化是一个昂贵的操作,尤其是在涉及复杂对象和大量数据时。频繁的反序列化可能会导致性能问题。

  • 最佳实践:

    • 如果可能,避免频繁进行反序列化操作,尤其是当数据只需要处理一次时,可以考虑通过其他机制(如数据库、缓存等)代替序列化。
    • 使用 Serializable 接口时,避免在每次需要数据时都进行反序列化,而是可以使用缓存策略来存储已序列化的对象。
  • 示例:

    java 复制代码
    // 通过缓存避免频繁反序列化
    public class Cache {
        private Map<String, Object> cache = new HashMap<>();
    
        public Object getObject(String key) {
            if (!cache.containsKey(key)) {
                // 反序列化操作
                cache.put(key, deserializeFromFile(key));
            }
            return cache.get(key);
        }
    }
7. 考虑序列化的性能
  • 原因:序列化和反序列化可能会带来性能问题,尤其是在需要序列化大型对象图时。通过优化序列化过程,可以提高性能。

  • 最佳实践:

    • 只序列化必要的字段,避免不必要的字段增加序列化的负担。
    • 在需要时,考虑使用更高效的序列化机制,如 Google 的 Protobuf 或 Apache Avro,这些工具提供了比 Java 默认序列化更高效的序列化格式。
  • 示例:

    java 复制代码
    // 使用 Google Protobuf 序列化数据(比 Java 默认序列化更高效)
    // Protobuf 定义和生成代码示例略
8. 选择合适的序列化机制
  • 原因:Java 默认的序列化机制虽然简单易用,但它的性能和兼容性在某些情况下可能不足。可以考虑使用其他序列化框架来提高性能。

  • 最佳实践:

    • 使用如 Google Protobuf、Kryo、Apache Avro 等高效的序列化库,这些库在性能和兼容性方面通常优于 Java 内建的序列化机制。
  • 示例:

    java 复制代码
    // 使用 Protobuf 序列化
    // Protobuf 序列化过程(示例代码略)

三、小结

《Effective Java》第8部分"序列化"强调了如何有效地使用 Java 的序列化机制,并介绍了多种优化和最佳实践。序列化虽然在许多应用场景中非常有用,但如果不加以控制和优化,可能会带来性能和兼容性问题。通过明确声明 serialVersionUID、控制序列化字段、避免不必要的反序列化、以及使用更高效的序列化框架,开发者可以编写出更加高效、可靠和可维护的序列化代

相关推荐
岑梓铭几秒前
考研408《计算机组成原理》复习笔记,第二章(3)数值数据的运算和存储(定点数计算)
笔记·考研·408·计算机组成原理
慕卿扬10 分钟前
基于python的机器学习(九)—— 评估算法(二)
python·学习·机器学习·scikit-learn
编程、小哥哥13 分钟前
互联网大厂Java求职面试实录 —— 严肃面试官遇到搞笑水货程序员
java·面试·mybatis·dubbo·springboot·多线程·juc
学游戏开发的32 分钟前
Lyra学习笔记1地图角色加载流程
笔记·学习·unreal engine
MounRiver_Studio37 分钟前
RISC-V IDE MRS2 开发笔记一:volatile关键字的使用
ide·笔记·risc-v
少可爱44 分钟前
对接钉钉消息样例:DING消息、机器人
java·开发语言·钉钉
xiaomu_3471 小时前
机器人强化学习入门学习笔记(三)
笔记·学习
Magnum Lehar1 小时前
vulkan游戏引擎的renderer下的vulkan缓冲区实现
java·前端·游戏引擎
虾球xz1 小时前
游戏引擎学习第309天:用于重叠检测的网格划分
c++·学习·算法·游戏引擎
鲁鲁5171 小时前
VS Code + Maven 创建项目
java·maven