1. 概述

有时,我们可能希望在 Java 方法中传递和修改一个String对象。例如,当我们想将另一个String追加到输入的String时,就会遇到这种情况。然而,输入变量在其所在方法的范围内有效。此外,String是不可变的。因此,如果我们不了解 Java 的内存管理,找到解决方案可能会有些模糊。

在这篇教程中,我们将理解如何将输入的String传递给方法。我们将了解如何使用StringBuilder,以及如何通过创建新对象来保持不可变性。

2. 值传递与引用传递

作为面向对象的语言,Java 可以定义基本类型和对象,并且它们可以存储在栈内存或堆内存中。此外,它们可以被值传递或引用传递到方法中。

2.1. 对象与基本类型

基本类型在栈内存中分配。当传递给方法时,我们得到的是基本类型的值副本。

对象是类模板的实例,存储在堆内存中。但在方法内部,程序可以通过引用访问它们,因为它拥有指向堆内存地址的引用。同样地,当将对象传递给方法时,我们得到的是对象引用的副本(可以将其视为指针)。

尽管传递基本类型或对象有所不同,但变量或对象在其所在方法的范围内有效。在这种情况下,总是发生共享调用,我们无法直接更新原始值或引用。因此,参数总是被复制的。

2.2. String 的不可变性

在 Java 中,String 是一个类而不是基本类型。因此,当传递其运行时实例时,我们在方法中会得到一个引用。

另外,它是不可变的。因此,即使我们想在方法内部操作String,除非创建一个新的,否则我们不能修改它。

3. 示例场景

在深入探讨一般解决方案之前,让我们先定义一个主要的应用场景。

假设我们想在方法中向输入的String追加内容。让我们看看方法执行前后会发生什么:

@Test
void givenAString_whenPassedToVoidMethod_thenStringIsNotModified() {
    String s = "hello";
    concatStringWithNoReturn(s);
    assertEquals("hello", s);
}

void concatStringWithNoReturn(String input) {
    input += " world";
    assertEquals("hello world", input);
}

concatStringWithNoReturn()方法内部,String得到了新的值。但是,在方法范围之外,我们仍然保留着原始值。

自然地,一个合理的解决方案是让方法返回一个新的String

@Test
void givenAString_whenPassedToMethodAndReturnNewString_thenStringIsModified() {
    String s = "hello";
    assertEquals("hello world", concatString(s));
}

String concatStringWithReturn(String input) {
    return input + " world";
}

值得注意的是,我们在安全地返回新实例的同时避免了副作用。

4. 使用StringBuilderStringBuffer

尽管String连接是一个选项,但使用StringBuilder(或StringBuffer的线程安全版本)是一种更好的实践。

4.1. StringBuilder

@Test
void givenAString_whenPassStringBuilderToVoidMethod_thenConcatNewStringOk() {
    StringBuilder builder = new StringBuilder("hello");
    concatWithStringBuilder(builder);

    assertEquals("hello world", builder.toString());
}

void concatWithStringBuilder(StringBuilder input) {
    input.append(" world");
}

我们追加到构建器的String暂时存储在一个字符数组中。因此,与String连接相比,这种做法的主要优点是性能。这样,我们就不会每次都创建一个新的String,而是等到我们有了所需的序列时,再进行所需的String转换。

4.2. StringBuffer

我们还有一个线程安全的版本,即StringBuffer。让我们看看它是如何工作的:

@Test
void givenAString_whenPassStringBufferToVoidMethod_thenConcatNewStringOk() {
    StringBuffer builder = new StringBuffer("hello");
    concatWithStringBuffer(builder);

    assertEquals("hello world", builder.toString());
}

void concatWithStringBuffer(StringBuffer input) {
    input.append(" world");
}

如果需要同步,这就是我们要使用的类。自然地,这可能会减慢进程,所以我们首先要理解这是否值得。

5. 处理对象属性

如果String是一个对象属性呢?

让我们定义一个简单的类来进行测试:

public class Dummy {

    String dummyString;
    // getter and setter
}

5.1. 使用设值器修改String状态

起初,我们可能会考虑简单地使用设值器来修改对象的String状态:

@Test
void givenObjectWithStringField_whenSetDifferentValue_thenObjectIsModified() {
    Dummy dummy = new Dummy();
    assertNull(dummy.getDummyString());
    modifyStringValueInInputObject(dummy, "hello world");
    assertEquals("hello world", dummy.getDummyString());
}

void modifyStringValueInInputObject(Dummy dummy, String dummyString) {
    dummy.setDummyString(dummyString);
}

值得注意的是,我们在堆内存中更新了原始对象的副本(仍然指向实际值)。

然而,这不是一个好的做法。它隐藏了String的变化。此外,如果多个线程尝试修改对象,可能会出现同步问题。

总的来说,只要可能,我们应该追求不可变性,并让方法返回一个新对象。

5.2. 创建新对象

当应用业务逻辑时,让方法返回新对象是一个好习惯。此外,我们也可以使用早些时候看到的StringBuilder模式来设置属性。让我们把这个例子总结起来:

@Test
void givenObjectWithStringField_whenSetDifferentValueWithStringBuilder_thenSetStringInNewObject() {
    assertEquals("hello world", getDummy("hello", "world").getDummyString());
}

Dummy getDummy(String hello, String world) {
    StringBuilder builder = new StringBuilder();

    builder.append(hello)
      .append(" ")
      .append(world);

    Dummy dummy = new Dummy();
    dummy.setDummyString(builder.toString());

    return dummy;
}

尽管这是一个简化示例,但我们可以看到代码更易读。此外,我们避免了副作用并保持了不可变性。任何方法的输入信息都是用来构造一个明确的新对象实例。

6. 总结

在这篇文章中,我们了解了如何在保持不可变性并避免副作用的同时改变方法的输入String。我们看到了如何使用StringBuilder,并将这个模式应用到新对象的创建中。

如往常一样,本文档中的代码可以在GitHub上找到。