Java中如何通过引用传递字符串?

191

我习惯在 C 中执行以下操作:

void main() {
    String zText = "";
    fillString(zText);
    printf(zText);
}

void fillString(String zText) {
    zText += "foo";
}

输出结果为:

foo

然而,在Java中,这似乎并不起作用。我认为这是因为String对象是复制而不是按引用传递。我以为String是对象,总是被按引用传递。

这里发生了什么?


3
即使它们是按引用传递的(在Java中,传递的是引用值的副本,但这是另一个主题),字符串对象仍然是不可变的,所以那也行不通。 - OscarRyz
15
不是C代码。 - Alston
@Phil:这在C#中也不会起作用。 - Mangesh
15个回答

231
你有三个选项:
  1. 使用 StringBuilder:

    StringBuilder zText = new StringBuilder ();
    void fillString(StringBuilder zText) { zText.append ("foo"); }
    
    创建一个容器类并将容器的一个实例传递给你的方法:
  2. public class Container { public String data; }
    void fillString(Container c) { c.data += "foo"; }
    
    创建一个数组:
    new String[] zText = new String[1];
    zText[0] = "";
    
    void fillString(String[] zText) { zText[0] += "foo"; }
    

    从性能角度来看,StringBuilder通常是最好的选择。


12
请记住,StringBuilder不是线程安全的。在多线程环境中,请使用StringBuffer类或手动确保同步。 - Boris Pavlović
15
@Boris Pavlovic - 是的,但是除非相同的 StringBuilder 被不同的线程使用(在任何情况下都不太可能),否则这不应该是一个问题。无论方法是否由不同的线程调用,接收不同的 StringBuilder 都没有关系。 - Ravi Wallau
我最喜欢第三个选项(数组),因为它是唯一通用的选项。它还允许传递布尔值、整数等。您能否详细解释一下这种解决方案的性能 - 您声称从性能角度来看第一个更好。 - meolic
1
@meolic StringBuilders += "..."更快,因为它不会每次都分配新的字符串对象。实际上,当你编写像s = "Hello " + name这样的Java代码时,编译器将生成字节码,创建一个StringBuilder并调用append()两次。 - Aaron Digulla

99
在Java中,没有任何东西是按引用传递的。一切都是按值传递。对象引用通过值传递。此外,字符串是不可变的。因此,当您追加传递的字符串时,您只会得到一个新的字符串。您可以使用返回值或传递StringBuffer来解决这个问题。

48
不,这是一个误解,你传递的不是对象的引用,而是值的引用。请注意不要改变原意,使翻译语言易于理解即可。 - Ed S.
35
是的,这是一个误解。它是一个巨大而普遍的错误观念。这导致了我讨厌的一个面试问题:(“Java如何传递参数”)。我讨厌它是因为大约一半的面试官似乎想要错误的答案(“基本类型按值传递,对象按引用传递”)。正确的答案需要更长时间来解释,并且似乎会让其中一些人感到困惑。他们不会被说服:我发誓我曾因为CSMajor类型的面试者在大学听到这个误解并把它当作真理而失败了技术面试。唉。 - CPerkins
4
@CPerkins,你不知道这让我多生气。这让我的血沸腾。 - anon
9
在所有编程语言中,所有东西都是按值传递的。其中一些值恰好是地址(引用)。 - gerardw
3
这是一个非常学究的观点,还是我有什么没看懂?我不明白这个答案为什么会被投赞成票,它只会让问题更加混淆。 - bnieland
显示剩余5条评论

56

发生的情况是,引用被按值传递,即复制了该引用。在Java中没有任何按引用传递的东西,而且由于字符串是不可变的,所以赋值操作创建了一个新的字符串对象,引用的副本现在指向它。原始引用仍然指向空字符串。

对于任何对象都是这样,即在方法中将其设置为新值。下面的示例只是更明显地说明了正在发生的事情,但连接字符串实际上是相同的操作。

void foo( object o )
{
    o = new Object( );  // original reference still points to old value on the heap
}

实际上,“引用的副本”被一个新的引用覆盖了。但是只针对局部变量。被调用的变量仍然保留着旧的引用。 - Humpity

18

我认为Soren Johnson不知道String是什么,这不是问题,也不是我在15年Java之后来到这里寻找的东西。 - AHH

12
所有Java中的参数都是按值传递。当你将一个String传递给一个函数时,传递的值实际上是指向一个String对象的引用,但是你不能修改该引用,而且底层的String对象是不可变的。
赋值操作
zText += foo;

等同于:

zText = new String(zText + "foo");

也就是说,它(在本地)重新分配参数zText作为一个新引用,该引用指向一个新的内存位置,在其中包含了原始的zText内容并附加了"foo"的新String

原始对象没有被修改,main()方法的本地变量zText仍然指向原始(空)字符串。

class StringFiller {
  static void fillString(String zText) {
    zText += "foo";
    System.out.println("Local value: " + zText);
  }

  public static void main(String[] args) {
    String zText = "";
    System.out.println("Original value: " + zText);
    fillString(zText);
    System.out.println("Final value: " + zText);
  }
}

输出:

Original value:
Local value: foo
Final value:

如果你想修改字符串,可以使用如下所述的 StringBuilder 或者其他容器(例如一个数组、AtomicReference 或自定义的容器类),这些容器提供了额外的指针间接级别。另外,你也可以直接返回新的值并将其赋值:

class StringFiller2 {
  static String fillString(String zText) {
    zText += "foo";
    System.out.println("Local value: " + zText);
    return zText;
  }

  public static void main(String[] args) {
    String zText = "";
    System.out.println("Original value: " + zText);
    zText = fillString(zText);
    System.out.println("Final value: " + zText);
  }
}

输出:

Original value:
Local value: foo
Final value: foo

这可能是在一般情况下最类似于Java的解决方案——请参见高效Java条目“偏爱不可变性”。

尽管如此,如果你需要大量的追加操作,特别是在循环内部,使用StringBuilder通常会给你更好的性能。

但如果可以,请尝试传递不可变的Strings而不是可变的StringBuilders——你的代码将更易读和更易维护。考虑将参数设置为final,并配置你的IDE使其在重新分配方法参数到新值时发出警告。


6
您说"严格来讲"好像不太相关,而它实际上是问题的核心所在。如果Java真正按引用传递参数,那就没有问题了。请尽量避免传播“Java按引用传递对象”的误解——我认为解释(正确的)“按值传递引用”模型会更有帮助。 - Jon Skeet
嘿,我的上一条评论去哪了? :-) 正如我之前所说,以及Jon现在补充的,真正的问题在于引用是按值传递的。这非常重要,许多人不理解语义上的差异。 - Ed S.
2
好的。作为一名Java程序员,我已经十年没有看到任何实际上通过引用传递的东西了,所以我的语言变得很随意。但你是对的,我应该更加小心,特别是对于C编程的受众。 - David Moles
这个答案清楚地解释了不可变的字符串对象的概念,按值传递、按引用传递和按引用的值传递。谢谢。 - Denny Hsu

8

对象是按引用传递的,基本数据类型是按值传递的。

String不是基本数据类型,它是一个对象,也是一种特殊情况的对象。

这是为了节省内存。在JVM中,有一个字符串池。对于每个创建的字符串,JVM都会尝试查看字符串池中是否存在相同的字符串,并在已有字符串的情况下指向它。

public class TestString
{
    private static String a = "hello world";
    private static String b = "hello world";
    private static String c = "hello " + "world";
    private static String d = new String("hello world");

    private static Object o1 = new Object();
    private static Object o2 = new Object();

    public static void main(String[] args)
    {
        System.out.println("a==b:"+(a == b));
        System.out.println("a==c:"+(a == c));
        System.out.println("a==d:"+(a == d));
        System.out.println("a.equals(d):"+(a.equals(d)));
        System.out.println("o1==o2:"+(o1 == o2));

        passString(a);
        passString(d);
    }

    public static void passString(String s)
    {
        System.out.println("passString:"+(a == s));
    }
}

/* 输出 */

a==b:true
a==c:true
a==d:false
a.equals(d):true
o1==o2:false
passString:true
passString:false

== 检查内存地址(引用),而 .equals 检查内容(值)。


3
不完全正确。Java始终按值传递;诀窍在于对象是通过引用传递的,这些引用是按值传递的(有点棘手)。请查看此链接:http://javadude.com/articles/passbyvalue.htm - adhg
2
@adhg,你写道“对象是通过引用传递的,这些引用是按值传递的。”<---那是无意义的。你的意思是对象具有引用,这些引用是按值传递的。 - barlop
2
你写的“对象是按引用传递的”<-- 这是错误的。如果这是真的,那么当调用一个方法并传递一个变量时,该方法可以使变量指向/引用不同的对象,但它不能。在Java中,所有东西都是按值传递的。而且你仍然可以看到在方法外更改对象属性的影响。 - barlop
请查看此处的答案:https://dev59.com/EXVD5IYBdhLWcg3wQJOT - barlop

6

在Java中,String是一个不可变的对象。您可以使用StringBuilder类来完成您想要完成的工作,具体如下:

public static void main(String[] args)
{
    StringBuilder sb = new StringBuilder("hello, world!");
    System.out.println(sb);
    foo(sb);
    System.out.println(sb);

}

public static void foo(StringBuilder str)
{
    str.delete(0, str.length());
    str.append("String has been modified");
}

另一种选择是创建一个带有字符串作为作用域变量的类(强烈不建议),如下所示:
class MyString
{
    public String value;
}

public static void main(String[] args)
{
    MyString ms = new MyString();
    ms.value = "Hello, World!";

}

public static void foo(MyString str)
{
    str.value = "String has been modified";
}

1
Java中的String不是原始类型。 - VWeber
真的,但你到底想要引起我的注意什么? - Fadi Hanna AL-Kass
Java字符串从何时开始成为原始类型?所以!字符串是按引用传递的。 - thedp
2
“不可变对象”是我想要表达的意思。由于某种原因,在@VWeber发表评论后,我没有重新阅读我的回答,但现在我明白他想说什么了。我的错误,已修改回答。 - Fadi Hanna AL-Kass

2

String是Java中的一个特殊类。它是线程安全的,这意味着“一旦创建了一个String实例,该实例的内容将永远不会改变”。

以下是其背后的原理:

 zText += "foo";

首先,Java编译器会获取zText字符串实例的值,然后创建一个新的字符串实例,其值为zText追加"foo"。因此,您知道为什么zText指向的实例没有改变。它完全是一个新实例。实际上,即使是字符串"foo"也是一个新的字符串实例。因此,对于这个语句,Java将创建两个字符串实例,一个是"foo",另一个是zText追加"foo"的值。 规则很简单:字符串实例的值永远不会改变。
对于fillString方法,您可以使用StringBuffer作为参数,或者像这样更改:
String fillString(String zText) {
    return zText += "foo";
}

虽然如果您按照此代码定义“fillString”,您应该给它一个更合适的名称! - Stephen C
是的。这个方法应该被命名为“appendString”或类似的名称。 - DeepNightTwo

2
答案很简单。在Java中,字符串是不可变的。因此,它就像使用“final”修饰符(或C / C ++中的“const”)一样。因此,一旦分配,您不能像您所做的那样更改它。
您可以更改字符串指向的值,但是您无法更改该字符串当前指向的实际值。
即。 String s1 =“hey”。您可以使s1 =“woah”,这是完全可以的,但是您无法实际更改字符串的基础值(在本例中为:“hey”),以便在使用plusEquals等时将其分配给其他内容。 (即s1 + =“whatup!= hey whatup”)。
要做到这一点,请使用StringBuilder或StringBuffer类或其他可变容器,然后只需调用.toString()将对象转换回字符串即可。
注意:字符串通常用作哈希键,因此这就是它们是不可变的原因之一。

无论它是可变的还是不可变的都不重要。对于引用上的 = 操作符,它永远不会影响原来指向的对象,无论该对象属于哪个类。 - newacct

1

这个可以使用StringBuffer来实现

public class test {
 public static void main(String[] args) {
 StringBuffer zText = new StringBuffer("");
    fillString(zText);
    System.out.println(zText.toString());
 }
  static void fillString(StringBuffer zText) {
    zText .append("foo");
}
}

更好的方法是使用 StringBuilder。
public class test {
 public static void main(String[] args) {
 StringBuilder zText = new StringBuilder("");
    fillString(zText);
    System.out.println(zText.toString());
 }
  static void fillString(StringBuilder zText) {
    zText .append("foo");
}
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接