在 Java 中创建不可变的自定义类

2024年9月10日 | 阅读 9 分钟

Java 中的不变性(Immutability)是指创建对象后,其状态无法被修改的概念。不变性在并发编程中尤其有用,因为它可以消除同步的需要并提供一定的线程保护。一种实现持续改进的方法是创建遵循一组指导方针的自定义类,以确保其模式在生命周期内保持一致。

不可变对象的特征

Java 中的不可变对象具有以下特征:

状态不可修改: 一旦创建了不可变对象,其状态就无法更改。所有字段都是 final 的,并且没有 setter 方法。

无修改器方法: 不可变对象没有修改其状态的方法。任何看似修改对象的操作都会返回一个带有更新值的新实例。

字段为 final: 不可变类中的所有字段都声明为 final,以确保在对象创建后无法重新赋值。

不允许通过子类修改: 为防止通过子类化进行潜在的修改,该类通常被标记为 final,或者其构造函数通过静态工厂方法设为私有。

创建不可变自定义类

让我们以一个假设的 Person 类为例,通过以下步骤创建 Java 中的不可变自定义类。

步骤 1:声明类为 Final

为防止子类化,将类声明为 final。

步骤 2:声明字段为 Final

确保所有字段都是 final。

步骤 3:提供构造函数

在构造函数中初始化所有字段,并避免直接暴露可变对象。

步骤 4:无 Setter 方法

避免提供 setter 方法或任何修改状态的方法。

步骤 5:防御性拷贝

如果类包含可变对象,请确保它们不能直接访问,并在需要时提供防御性拷贝。

步骤 6:返回克隆或不可变对象

从方法返回可变对象时,返回克隆或不可变实例以保持不变性。

步骤 7:重写 equals() 和 hashCode()

重写 equals() 和 hashCode() 方法,以确保在使用集合中的实例时行为正确。

步骤 8:枚举 (Enum) 的不变性

Java 中的枚举 (Enum) 是隐式 final 的,并且具有固定的实例,这使得它们非常适合不变性。如果我们有一组常量值,请考虑使用枚举。

步骤 9:懒惰初始化

如果对象的创建成本很高,并且其值不一定始终需要,我们可以采用懒惰初始化。仅在请求时计算并缓存值。

步骤 10:有效使用 final 关键字

除了将字段标记为 final 外,还可以考虑在适用时使用 final 关键字修饰方法和类。将方法标记为 final 可以防止子类重写它们,从而为您的设计增加额外的保护层。

步骤 11:不变性与集合

在处理集合时,请确保集合中的元素也是不可变的,或者提供防御性拷贝以保持类的不变性。Java 提供了 Collections.unmodifiableList() 和 Collections.unmodifiableMap() 等实用类来创建集合的不可修改视图。

步骤 12:序列化

为了使不可变类能够正确序列化,请确保所有字段都可序列化。实现 Serializable 接口,并确保字段在引用不可序列化对象时被标记为 transient。

步骤 13:有效使用 Objects 类

Java 的 Objects 类提供了处理 null 值以及处理 equals 和 hash code 操作的实用方法。使用这些方法可以简化您的代码并使其更简洁。

步骤 14:命名约定的一致性

对不可变类、方法和字段遵循一致的命名约定。这有助于提高代码的可读性和可维护性。

现在,让我们创建一个不可变的 Person 类的完整示例,以及一个演示其用法的简单程序。以下是代码:

文件名:Person.java

现在,让我们创建一个简单的 Main 类来演示如何使用 Person 类。

文件名:CustomImmutable.java

输出

Person 1: Person{name='Alice', age=30}
Person 2: Person{name='Bob', age=25}
Are person1 and person2 equal? false
HashCode of person1: -1061542078
HashCode of person2: -595926066

正如我们所见,我们成功地创建了 Person 类的实例,演示了创建后其状态无法被修改,并展示了如何实现 equality 和 hashCode() 方法。输出也证实了这些实例是不可变的,因为尝试修改它们的状态会导致编译错误。

要保存和运行 Java 程序,请遵循以下步骤:

保存程序

打开文本编辑器: 您可以使用任何您喜欢的文本编辑器,例如记事本(Windows)、TextEdit(macOS)或任何代码编辑器,如 Visual Studio Code、IntelliJ IDEA 或 Eclipse。

复制代码: 复制上面提供的 Person.java 和 CustomImmutable.java 代码,并将它们粘贴到文本编辑器中的单独文件中。将 Person.java 文件保存为 Person.java,将 CustomImmutable.java 文件保存为 CustomImmutable.java。

选择一个目录: 将这两个文件保存在计算机上您选择的任何目录(文件夹)中。为您的 Java 项目创建一个专用文件夹是一个好习惯。

运行程序

打开终端或命令提示符: 导航到您保存 Java 文件的目录。您可以使用终端或命令提示符中的 cd 命令来完成此操作。

编译 Java 文件: 使用 javac 命令编译 Person.java 和 CustomImmutable.java 文件。它将生成相应的 .class 文件。

运行程序: 成功编译后,使用 java 命令执行 CustomImmutable 类来运行程序。

查看输出: 程序将执行,我们将在终端或命令提示符中看到打印的输出。

让我们深入探讨 Java 中不可变类的某些高级概念和最佳实践。

1. 有效使用 LocalDateTime 和 ImmutableList

在处理 Java 中的日期和时间时,请考虑使用 java.time 包中的 LocalDateTime,它是不可变的且线程安全的。同样,在处理集合时,请使用 Guava 库中的 ImmutableList 来确保不变性。

2. 使用 java.util.concurrent 实现线程安全

对于并发应用程序,请使用 java.util.concurrent 包中的类来确保线程安全。例如,AtomicInteger 和 AtomicReference 分别为整数值和对象引用提供原子操作。

3. 使用 Builder 模式创建不可变对象

利用 Builder 模式来构建具有许多属性的复杂不可变对象。此模式允许灵活地创建对象,同时确保不变性。

4. 不变性与性能

不变性有时可以通过减少同步的需要并允许 JVM 进行更好的优化来提高性能。但是,在处理大型对象或频繁的状态更改时要小心,因为创建新实例可能会导致内存使用量增加和垃圾回收开销。

5. 防御性拷贝与外部可变性

当从不可变类返回可变对象时,请确保进行防御性拷贝,以防止外部修改。这适用于集合、数组和其他可变对象。

结论

在 Java 中创建不可变自定义类需要仔细选择策略,以确保类实例在创建后无法更改。遵循上述指南可以产生健壮且简单的规则,这在并发编程中很重要。采用不变性可以让开发人员编写易于理解、维护和评估的代码。