Java 中具有可变对象的不可变类

10 Sept 2024 | 5 分钟阅读

在面向对象编程领域,不变性是一个强大的概念,可以提高代码的健壮性、线程安全性和整体程序稳定性。不可变类是指创建后其实例无法被修改的类。虽然不变性带来了诸多优势,但在某些情况下,我们需要在 Java 的不可变类中处理可变对象。这种微妙的平衡允许开发人员在不损害不变性优势的前提下,设计出灵活高效的系统。

不可变性

不变性是通过将类的字段声明为 final 并通过构造函数或声明时进行初始化来实现的。一旦对象被创建,其状态就无法更改,从而确保了线程安全并简化了代码行为的理解。不可变类因其在并发编程中的易用性以及避免意外副作用而常被优先选择。

不可变对象的可变性挑战

然而,在某些情况下,仅使用不可变类可能不切实际。考虑一些情况,例如对象的某个字段由于性能原因需要可变,或者需要与需要状态修改的外部系统进行交互。在这种情况下,在否则不可变的类中包含可变对象就成为一种必要。

处理不可变类中可变对象的策略

防御性拷贝

一种常见的策略是在构造期间创建可变对象的防御性拷贝。这可以确保即使原始可变对象在别处被修改,不可变类的内部状态仍然保持不变。这种方法可以保持不变性的优势,同时允许对可变对象的受控操作。

包装类

另一种方法是使用包装类来封装可变对象并提供对其方法的受控访问。这样,内部状态的修改就可以在不可变类本身中进行管理。

ImmutableClassWithMutableObject.java

让我们更深入地探讨一下在 Java 中处理不可变类和可变对象时的一些注意事项和最佳实践。

1. 线程安全和不变性

不变性的主要好处之一是提高了线程安全性。不可变对象可以安全地在多个线程之间共享,而无需锁定。在不可变类中包含可变对象时,请确保可变状态得到妥善封装或同步,以保持线程安全。

2. 有效的防御性拷贝

创建防御性拷贝时,考虑拷贝的深度至关重要。如果可变对象包含对其他可变对象的引用,则可能需要深度拷贝以防止意外修改。此外,根据用例仔细选择用于防御性拷贝的适当集合类。

3. 不可变集合

Java 在 Collections 类中提供了不可变集合,例如 unmodifiableList、unmodifiableSet 和 unmodifiableMap。这些可以用来包装可变集合并提供不可变的视图。但是,请小心,因为这些包装器不会阻止修改原始集合。

4. 用于可变初始化的 Builder 模式

如果构造一个具有多个字段的对象,其中一些字段是可变的,请考虑使用 Builder 模式。Builder 可以在构造阶段处理可变状态,从而确保最终对象是不可变的。

5. 文档和合同

清楚地记录类的不变性合同,尤其是在涉及可变对象时。明确说明对可变对象的修改是否会影响包含它的不可变类的状态。提供此类信息有助于类用户做出明智的决定。

6. 命名一致性

为与可变对象相关的方法维护一致的命名约定。例如,如果我们选择将获取可变对象的方法命名为 getMutableObject(),请在整个代码库中保持一致。

7. 不变性测试

包含测试以确保类的不可变性,尤其是在涉及可变对象时。测试内部状态在可变对象受到外部修改时保持不变的场景。

通过仔细考虑这些方面并应用最佳实践,开发人员可以创建健壮且灵活的系统,在处理 Java 中的可变对象时,可以利用不变性和可控可变性的优势。

让我们创建一个简单的示例来演示 Java 中带有可变对象的不可变类。在此示例中,我们将使用一个不可变类来表示一名学生,可变对象将是学生注册的课程列表。

文件名:ImmutableStudent.java

输出

Student Name: John Doe
Enrolled Courses: [Mathematics, Physics]
After modifying the original list:
Student Name: John Doe
Enrolled Courses: [Mathematics, Physics]

在此示例中,ImmutableStudent 类通过在构造期间创建已注册课程列表的防御性拷贝来确保不变性。getEnrolledCourses() 方法返回列表的不可修改视图,防止外部修改。main() 方法演示了即使在创建学生对象后修改了原始课程列表,学生对象的状态也不会改变。

结论

虽然不变性是一个强大的概念,但在某些情况下,在不可变类中包含可变对象是必要的。通过采用防御性拷贝或使用包装类等策略,开发人员可以在不变性与可变状态的需求之间取得平衡。仔细考虑和周到的设计对于确保在满足 Java 中可变对象的特定要求的同时,保持不变性的优势至关重要。