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


160

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

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

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

输出为:

foo

但是,在Java中,这似乎不起作用。我认为是因为该String对象是复制的,而不是通过referenced传递的。我以为字符串是对象,总是通过引用传递。

这里发生了什么?


2
即使它们是通过引用传递的(在Java中传递的是引用值的副本,但这是另一个线程)字符串对象是不可变的,因此无论如何都无法工作
OscarRyz

8
这不是C代码。
2014年

@Phil:这在C#中也不起作用。
Mangesh '16

Answers:


196

您有三种选择:

  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"; }
  3. 创建一个数组:

    new String[] zText = new String[1];
    zText[0] = "";
    
    void fillString(String[] zText) { zText[0] += "foo"; }

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


11
请记住,StringBuilder不是线程安全的。在多线程环境中,请使用StringBuffer类或手动进行同步。
鲍里斯·帕夫洛维奇

12
@Boris Pavlovic-是的,但是除非同一个StringBuilder被不同的线程使用(在任何给定的情况下IMO不太可能使用),否则这不是问题。该方法是否由接收不同StringBuilder的不同线程调用并不重要。
拉维·瓦劳

我最喜欢第三个选项(数组),因为它是唯一的通用选项。它还允许传递boolean,int等。您能否详细解释一下该解决方案的性能-您声称从性能的角度来看,第一个更好。
meolic

@meolic StringBuilder更快,s += "..."因为它不会每次都分配新的String对象。实际上,当您编写Java代码(如)时s = "Hello " + name,编译器将生成字节码,该字节码创建a StringBuilder并调用append()两次。
亚伦·迪古拉

85

在Java中,没有任何东西通过引用传递。一切都是通过价值传递的。对象引用按值传递。另外,字符串是不可变的。因此,当您附加到传递的字符串时,您将获得一个新的字符串。您可以使用返回值,也可以传递StringBuffer。


2
这不是一个误解,而是一个概念
Patrick Cornelissen

41
嗯..不,这是一个误解,认为您正在通过引用传递对象。您正在按值传递参考。
Ed S.

30
是的,这是一个误解。这是一个巨大的普遍误解。这导致了我讨厌的采访问题:(“ Java如何传递参数”)。我讨厌它,因为大约一半的面试官实际上似乎想要错误的答案(“按价值划分的原始事物,按参考划分的事物”)。正确的答案需要更长的时间才能给出,而且似乎使其中的一些困惑。而且他们不会被说服:我发誓我没有通过科技类筛查,因为CSMajor型筛查员听说过大学时的误解并将其视为福音。FEH。
CPerkins,2009年

4
@CPerkins你不知道这让我多么生气。它使我沸腾。

6
一切都通过价值传递给所有语言。其中一些值恰好是地址(引用)。
gerardw 2014年

52

发生的事情是按值传递引用,即,传递引用的副本。java中的任何内容都不会通过引用传递,并且由于字符串是不可变的,因此该分配创建了一个新的字符串对象,引用的副本现在指向对象。原始引用仍指向空字符串。

对于任何对象,这都是相同的,即在方法中将其设置为新值。下面的示例使事情变得更加显而易见,但是连接字符串实际上是一回事。

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

6
很好的例子!
Nickolas 2012年


8

对象通过引用传递,基元通过值传递。

字符串不是原始的,它是一个对象,并且是对象的一种特殊情况。

这是为了节省内存。在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总是通过VALUE传递;诀窍是对象按引用传递,按值传递。(很奇怪,我知道)。请检查以下内容:javadude.com/articles/passbyvalue.htm
adhg 2015年

1
@adhg,您写了“对象是按引用传递的,即按值传递的。” <---那是胡言乱语。您的意思是对象具有按值传递的引用
barlop

2
-1您编写了“通过引用传递对象”,<-否。如果这是真的,那么在调用方法并传递变量时,该方法可以使变量指向/引用其他对象,但事实并非如此。一切都通过java中的值传递。而且,您仍然可以看到在方法之外更改对象属性的效果。
barlop


7

Java中的所有参数均按值传递。当您将a传递String给函数时,传递的值对String对象的引用,但是您不能修改该引用,并且基础String对象是不可变的。

那作业

zText += foo;

等效于:

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

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

原始对象没有被修改,并且该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通常会为您带来更好的性能-如果您有很多附加工作要做,尤其是在循环内,请使用StringBuilder

但是,如果可以的话,请尝试传递不可变Strings而不是可变的StringBuilders-您的代码将更易于阅读和维护。考虑制作参数final,并配置IDE以在将方法参数重新分配给新值时向您发出警告。


6
您说“严格地说”似乎几乎无关紧要-而这是问题的核心。如果Java 确实通过引用传递了,那将不是问题。尝试避免传播“ Java通过引用传递对象”的神话-IMO解释(正确的)“引用按值传递”模型更加有用。
乔恩·斯基特

嘿,我先前的评论去了哪里?:-)就像我之前说的那样,正如乔恩现在所说的那样,这里的真正问题是引用是按值传递的。这非常重要,许多人不了解语义上的区别。
Ed S.

1
很公平。作为Java程序员,十年来我从未见过任何通过引用实际通过的东西,因此我的语言变得草率。但是您是对的,我应该多加注意,尤其是对于C编程的读者。
David Moles

5

字符串是Java中的不可变对象。您可以使用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");
}

另一个选择是使用String作为范围变量(强烈建议)创建一个类,如下所示:

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中的原始类型。
VWeber 2014年

是的,但是您到底想吸引我什么?
Fadi Hanna AL-Kass 2014年

从什么时候开始Java String是原始类型?所以!通过引用传递的字符串。
thedp 2014年

2
我打算说的是“不可变的对象”。@VWeber发表评论后,由于某种原因,我没有重新阅读我的回答,但是现在我明白了他想说的话。我的错。答案修改
法迪汉娜AL-卡斯

2

答案很简单。在Java中,字符串是不可变的。因此,它就像使用'final'修饰符(或C / C ++中的'const')一样。因此,一旦分配,就不能像以前那样进行更改。

您可以更改值到串点,但你不能改变此字符串当前所指向的实际值。

就是 String s1 = "hey"。您可以进行s1 = "woah",这完全可以,但是实际上您不能将字符串的基础值(在这种情况下:“嘿”)更改为使用plusEquals等进行分配后的其他值(即s1 += " whatup != "hey whatup")。

为此,请使用StringBuilder或StringBuffer类或其他可变容器,然后只需调用.toString()即可将对象转换回字符串。

注意:字符串通常用作哈希键,因此这是字符串不可变的部分原因。


它是可变的还是不变的无关紧要。=在引用上,无论使用什么类,都不会影响它过去指向的对象。
newacct 2014年

2

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

这是怎么回事

 zText += "foo";

首先,Java编译器将获取zText String实例的值,然后创建一个新的String实例,其值是zText并附加“ foo”。因此,您知道zText指向的实例为何未更改。这完全是一个新实例。实际上,甚至字符串“ foo”都是新的String实例。因此,对于此语句,Java将创建两个String实例,一个是“ foo”,另一个是zText追加“ foo”的值。规则很简单:String实例的值永远不会改变。

对于方法fillString,可以将StringBuffer用作参数,也可以像这样更改它:

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

...虽然如果你要定义“fillString”按照这个代码,你真的应该给它一个更合适的名称!
Stephen C

是。此方法应命名为“ appendString”左右。
DeepNightTwo


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");
}
}

1

字符串在Java中是不可变的。您不能修改/更改现有的字符串文字/对象。

字符串s =“ Hello”; s = s +“ hi”;

在此,先前的引用s替换为指向值“ HelloHi”的新引用s。

但是,为了带来可变性,我们有StringBuilder和StringBuffer。

StringBuilder s = new StringBuilder(); s.append(“ Hi”);

这会将新值“ Hi”附加到相同的引用s。//


0

到目前为止,亚伦·迪古拉(Aaron Digulla)的答案是最好的。他的第二个选择的一种变体是使用Common lang库版本3+的包装器或容器类MutableObject:

void fillString(MutableObject<String> c) { c.setValue(c.getValue() + "foo"); }

保存容器类的声明。缺点是对commons lang lib的依赖。但是lib具有很多有用的功能,并且我从事过的几乎所有大型项目都使用过它。


0

为了通过java中的引用传递对象(包括String),您可以将其作为周围适配器的成员传递。具有泛型的解决方案在这里:

import java.io.Serializable;

public class ByRef<T extends Object> implements Serializable
{
    private static final long serialVersionUID = 6310102145974374589L;

    T v;

    public ByRef(T v)
    {
        this.v = v;
    }

    public ByRef()
    {
        v = null;
    }

    public void set(T nv)
    {
        v = nv;
    }

    public T get()
    {
        return v;
    }

// ------------------------------------------------------------------

    static void fillString(ByRef<String> zText)
    {
        zText.set(zText.get() + "foo");
    }

    public static void main(String args[])
    {
        final ByRef<String> zText = new ByRef<String>(new String(""));
        fillString(zText);
        System.out.println(zText.get());
    }
}

0

对于更好奇的人

class Testt {
    static void Display(String s , String varname){
        System.out.println(varname + " variable data = "+ s + " :: address hashmap =  " + s.hashCode());
    }

    static void changeto(String s , String t){
        System.out.println("entered function");
        Display(s , "s");
        s = t ;
        Display(s,"s");
        System.out.println("exiting function");
    }

    public static void main(String args[]){
        String s =  "hi" ;
        Display(s,"s");
        changeto(s,"bye");
        Display(s,"s");
    }
}

现在,通过运行以上代码,您可以看到hashcodesString变量s 如何改变地址。changeto更改s后,将新对象分配给函数中的变量s

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.