Java中可变对象与不可变对象

在 Java 中使用对象时,了解可变对象和不可变对象之间的区别至关重要。这些概念影响 Java 代码的行为和设计。

在本教程中,我们将探讨可变对象和不可变对象的定义、示例、优点和注意事项。

什么是不可变对象
不可变对象是指一旦创建其状态就无法更改的对象。一旦不可变对象被实例化,它的值和属性在其整个生命周期中保持不变。

让我们探讨一些 Java 中内置不可变类的示例。

1.字符串类
Java中字符串的不可变性保证了线程安全,增强了安全性,并通过字符串池机制帮助高效利用内存。

@Test
public void givenImmutableString_whenConcatString_thenNotSameAndCorrectValues() {
    String originalString = "Hello";
    String modifiedString = originalString.concat(
" World");
    assertNotSame(originalString, modifiedString);
    assertEquals(
"Hello", originalString);
    assertEquals(
"Hello World", modifiedString);
}

在此示例中,concat()方法创建一个新的String,而原始String保持不变。

2.整数类
在 Java 中,Integer类是不可变的,这意味着它的值一旦设置就无法更改。但是,当您对Integer执行操作时,会创建一个新实例来保存结果。

@Test
public void givenImmutableInteger_whenAddInteger_thenNotSameAndCorrectValue() {
    Integer immutableInt = 42;
    Integer modifiedInt = immutableInt + 8;
    assertNotSame(immutableInt, modifiedInt);
    assertEquals(42, (int) immutableInt);
    assertEquals(50, (int) modifiedInt);
}

这里,+操作创建了一个新的Integer对象,并且原始对象保持不可变。

3.不可变对象的优点
Java 中的不可变对象具有多种优势,有助于提高代码可靠性、简单性和性能。让我们了解使用不可变对象的一些好处:

  • 线程安全:不变性本质上保证了线程安全。由于不可变对象的状态在创建后无法修改,因此可以在多个线程之间安全地共享它,而不需要显式同步。这简化了并发编程并降低了竞争条件的风险。
  • 可预测性和调试:不可变对象的恒定状态使代码更具可预测性。创建后,不可变对象的值保持不变,从而简化了代码行为的推理。
  • 促进缓存和优化:不可变对象可以轻松缓存和重用。一旦创建,不可变对象的状态就不会改变,从而允许高效的缓存策略。

因此,开发人员可以在 Java 应用程序中使用不可变对象来设计更健壮、可预测且高效的系统。

创建不可变对象
要创建不可变对象,让我们考虑一个名为ImmutablePerson的类的示例。该类被声明为final以防止扩展,并且它包含私有final字段,没有setter方法,遵循不变性原则。

public final class ImmutablePerson {
    private final String name;
    private final int age;
    public ImmutablePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }
}

现在,让我们考虑一下当我们尝试修改ImmutablePerson实例的名称时会发生什么:

ImmutablePerson person = new ImmutablePerson("John", 30);
person.setName(
"Jane"); 

尝试修改ImmutablePerson实例的名称将导致编译错误。这是因为该类被设计为不可变的,没有允许在实例化后更改其状态的 setter 方法。

setter 的缺失以及将类声明为Final确保了对象的不变性,从而提供了一种清晰而健壮的方法来处理整个生命周期中的常量状态。

可变对象
Java 中的可变对象是其状态在创建后可以修改的实体。这种可变性引入了可变内部数据的概念,允许在对象的生命周期中更改值和属性。

让我们探讨几个例子来了解它们的特征。

1.字符串生成器类
Java 中的StringBuilder类表示可变的字符序列。与它的不可变对应物String不同,StringBuilder允许动态修改其内容。

@Test
public void givenMutableString_whenAppendElement_thenCorrectValue() {
    StringBuilder mutableString = new StringBuilder("Hello");
    mutableString.append(
" World");
    assertEquals(
"Hello World", mutableString.toString());
}

这里,append方法直接改变StringBuilder对象的内部状态,展示了它的可变性。

2.数组列表类
ArrayList类是可变对象的另一个示例。它表示一个动态数组,可以增大或缩小大小,从而允许添加和删除元素。

@Test
public void givenMutableList_whenAddElement_thenCorrectSize() {
    List<String> mutableList = new ArrayList<>();
    mutableList.add("Java");
    assertEquals(1, mutableList.size());
}

add方法通过添加元素来修改ArrayList的状态,体现了其可变性质。

3.注意事项
虽然可变对象提供了灵活性,但开发人员需要注意它们的某些注意事项:

  • 线程安全:可变对象可能需要额外的同步机制来确保多线程环境中的线程安全。如果没有适当的同步,并发修改可能会导致意外的行为。
  • 代码理解的复杂性:修改可变对象的内部状态的能力引入了代码理解的复杂性。开发人员需要谨慎对待对象状态的潜在更改,尤其是在大型代码库中。
  • 状态管理挑战:管理可变对象的内部状态需要仔细考虑。开发人员应该跟踪和控制更改,以确保对象的完整性并防止意外修改。

尽管有这些考虑,可变对象提供了一种动态且灵活的方法,允许开发人员根据不断变化的需求调整对象的状态。

在可变性和不变性之间进行选择
可变性和不变性之间的选择取决于应用程序的需求。如果需要适应性和频繁更改,请选择可变对象。然而,如果一致性、安全性和稳定状态是优先考虑的因素,那么不变性就是最佳选择。

考虑多任务场景中的并发方面。不变性简化了任务之间的数据共享,而无需复杂的同步。

此外,评估您的应用程序的性能需求。虽然不可变对象通常会提高性能,但请权衡这种提升是否比可变对象提供的灵活性更重要,尤其是在数据更改不频繁的情况下。

保持适当的平衡可确保您的代码有效地满足应用程序的需求。