在 Java 中透過引用傳遞字串
1. 概述
有時,我們可能想在 Java 的方法中傳遞和修改String
。例如,當我們想要將另一個String
附加到輸入中的字串時,就會發生這種情況。但是,輸入變數在方法內有其作用域。此外, String
是不可變的。因此,如果我們不了解 Java 記憶體管理,那麼尋找解決方案可能會令人困惑。
在本教程中,我們將了解如何將輸入String
傳遞給方法。我們將了解如何使用StringBuilder
以及如何透過建立新物件來保持不變性。
2. 按值或引用傳遞
作為一種 OOP 語言,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. 使用StringBuilder
或StringBuffer
儘管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.使用 Setter 修改String
狀態
首先,我們可以想到簡單地使用setter來修改物件的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;
}
雖然這是一個簡化的範例,但我們可以看到程式碼如何更具可讀性。此外,我們避免副作用並保持不變性。方法的任何輸入資訊都是我們用來建構新物件的明確標識實例的資訊。
六,結論
在本文中,我們了解如何更改方法的輸入String
,同時保持不變性並避免副作用。我們了解如何使用StringBuilder
並在新物件建立中應用此模式。
與往常一樣,本文中提供的程式碼可以在 GitHub 上取得。