关于字符串拼接行为

4

我明白,考虑到字符串的不可变性,像

String a="";
for(int i=0;i++<9;)
    a+=i;

这种方式非常低效,因为首先会实例化一个字符串并将其放入字符串池中,然后使用a+=i创建一个新的字符串(第一个循环中是0),由a引用,之前的字符串现在可以进行垃圾回收。而这个过程会发生九次。

更好的方法是使用StringBuilder

StringBuilder a=new StringBuilder("");
for(int i=0;i++<9;)
    a.append(i);

但是当我使用new关键字实例化字符串时,背后会发生什么呢?

String a=new String("");
for(int i=0;i++<9;)
    a+=i;

我知道在这种情况下,a没有被interned(它不在字符串池中),但它仍然是不可变的吗?在这种情况下,a+=i指令会做什么?它的行为是否与我的第一个示例相同?


6
你怎么会认为在你的第一个版本中所有的字符串都被放入了池子里呢?只有空字符串会在字符串池中...其他的字符串都会在堆上。是的,所有的字符串都是不可变的,无论它们是否来自常量池。 - Jon Skeet
@JonSkeet 感谢您的澄清... 我有点困惑。 - Luigi Cortese
2个回答

4

只有字符串字面值或者你在上调用intern()方法的String才会被放入字符串池中。字符串连接不会自动将String加入字符串池,所以关于字符串池,你的示例是相同的。

String abc = new String("abc"); //"abc" is put on the pool
abc += "def"; //"def" is put on the pool, but "abcdef" is not
String xyz = "abcdefghi".substring(0, 6).intern(); //"abcdef" is now added to the pool and returned by the intern() function
String xyz = "test"; //test is put on the pool
xyz += "ing"; //ing is put on the pool, but "testing" is not

进一步讲解,需要注意的是String构造器不会自动将字符串池中的值intern(或者不intern)。使用字符串字面量(即代码中用引号括起来的字符串)才能将字符串加入到字符串池中。

String abc = "abc"; //"abc" is in the pool
String def = "def"; //"def" is in the pool
String str1 = new String(abc + def); //"abcdef" is not in the pool yet
String str2 = new String("abcdef"); //"abcdef" is on the pool now

请注意,String的复制构造函数几乎从不使用,因为字符串本身是不可变的。
要了解更多信息,请阅读这里这里这里的答案。

清楚了!我以为字符串的内部化过程与其不可变性之间存在联系。 - Luigi Cortese
字符串"abc"是一个字符串字面量,因此它被放在字符串池中。构造函数不会自动将字符串进行内部化或者不内部化。 - Kevin Workman
所以,你的意思是字面量“abc”进入池中,但变量abc不会进入池中? - Luigi Cortese
通过阅读像这样的答案https://dev59.com/1nI-5IYBdhLWcg3wYnSQ,我认为`new String("abc");`不会将“abc”放入池中... - Luigi Cortese
@LuigiCortese 字符串常量 "abc" 被放入池中。构造函数返回的字符串是该字符串的副本。 - Kevin Workman
显示剩余3条评论

3

让我们考虑您的第一个例子:

String a="";
for(int i=0;i++<9;)
    a+=i;

这段代码会按照以下方式执行:
String a="";
a=new StringBuilder(a).append(0).toString();
a=new StringBuilder(a).append(1).toString();
a=new StringBuilder(a).append(2).toString();
a=new StringBuilder(a).append(3).toString();
a=new StringBuilder(a).append(4).toString();
a=new StringBuilder(a).append(5).toString();
a=new StringBuilder(a).append(6).toString();
a=new StringBuilder(a).append(7).toString();
a=new StringBuilder(a).append(8).toString();
a=new StringBuilder(a).append(9).toString();

所以,对于每个循环迭代,您将从字符串创建一个新的StringBuilder(每次分配一个新的内部char[]缓冲区),每次都会将其转换回String。另一方面,在第二种情况下,您实际上将拥有

StringBuilder a=new StringBuilder("").append(0).append(1).append(2).append(3)
    .append(4).append(5).append(6).append(7).append(8).append(9);

因此,您只会有一个 StringBuilder,只有一个内部的 char[] 缓冲区(在您的情况下,它不会被重新分配,因为所有附加都没有超过 16 的初始容量)。所以它更快,因为您有较少的对象,并且没有重复复制任何内容。
a=new String("") 是无用的,因为除了已经存在的空字符串之外,您还将拥有一个额外的空字符串(在加载类常量池时创建和interned)。除了多余的空字符串(在第一次循环迭代后变得未使用并被垃圾回收)之外,它与第一种情况没有区别。
请注意,JLS 没有精确规定字符串连接是如何实现的(使用隐式的 StringBuilder 或其他技术),但通常 java 编译器会使用 StringBuilder 进行转换。

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