新建字符串对象与字符串字面量性能比较

19

这个问题在StackOverflow上已经被问了很多次,但是它们都没有以性能为基础。

在《Effective Java》一书中指出:

如果String s = new String("stringette");在循环或者一个经常调用的方法中出现,那么成千上万的String实例会被不必要地创建。

改进后的版本简单得多:String s = "stringette";这个版本只使用一个String实例,而不是每次执行时都创建一个新的。

因此,我尝试了两种方法,并发现性能方面有显著的改善

for (int j = 0; j < 1000; j++) {
    String s = new String("hello World");
}

需要大约399 372纳秒的时间。

for (int j = 0; j < 1000; j++) {
    String s = "hello World";
}

需要23,000纳秒左右。

为什么性能有如此大的提升?是否有内部发生了编译器优化


6
哦,正如你从书中引用的原因所述。而且这还忽略了你的测试不太可能产生任何有意义的结果。请参见:https://dev59.com/hHRB5IYBdhLWcg3wz6UK - Brian Roach
6
像这样进行 1,000 次迭代测试是不可行的——它很可能会产生与真实情况不符的结果... - assylias
@BrianRoach 是的,那是真的。混淆的源头在于到处都说字符串字面量是“对象字面量”,但我认为对象字面量是动态的而不是静态的。 - user1825817
为什么它们的性能不应该有所不同呢?毕竟它们执行的是完全不同的任务。 - Tony Hopkinson
1
@cSharper - 字符串并不是静态的,它只是一组实例池。Immutable(不可变)并不等同于static(静态)。 - Brian Roach
4个回答

41
在第一种情况下,每次迭代都会创建一个新的对象;而在第二种情况下,始终使用相同的对象,并从 String 常量池中检索该对象。
在 Java 中,当你执行以下操作时:
String bla = new String("xpto");

你强制创建了一个新的字符串对象,这会占用一些时间和内存。

另一方面,如果你执行以下操作:

String muchMuchFaster = "xpto"; //String literal!

字符串(String)只会在第一次创建时生成新对象并缓存在字符串常量池中,因此每次以其文字形式引用它时,您都会获得完全相同的对象,这是非常快速的。

现在你可能会问……如果代码中的两个不同点检索相同的文字并更改它,难道不会出现问题吗?!

不会,因为在Java中,字符串(Strings)是不可变的!因此,任何会改变字符串的操作都会返回一个新的字符串,使得任何其他对相同文字的引用可以保持不变。

这是不可变数据结构的优势之一,但这是另一个问题,我可以写几页关于它。

编辑

只是澄清一下,常量池不仅限于字符串类型,您可以在这里阅读更多信息,或者如果您在谷歌搜索Java常量池。

http://docs.oracle.com/javase/specs/jvms/se7/jvms7.pdf

此外,您可以进行以下简单测试来理解:

String a = new String("xpto");
String b = new String("xpto");
String c = "xpto";
String d = "xpto";

System.out.println(a == b);
System.out.println(a == c);
System.out.println(c == d);

通过这一切,你可能已经能够想象出这些Sysouts的结果:

false
false
true

由于cd是同一对象,所以==比较为真。


你有支持这个的参考资料吗? - Aaron Kurtzhals
我会满意于提供Java语言规范中相关章节的参考。 :) - Aaron Kurtzhals
@MarkoTopolnik 没错,我通过添加Java7 pdf版本的链接进行了更正。 - pcalcao
1
请注意,即使是第一次,该字符串也不会被创建:它已经在类加载时创建。就像类的“资源”一样。 - Marko Topolnik
我想我应该阅读Java的语言规范,以更好地了解这门美丽的语言。谢谢大家。 - user1825817
显示剩余5条评论

4
事实上,性能差异要大得多:HotSpot编译整个循环的难度较小。
for (int j = 0; j < 1000; j++)
{String s="hello World";}

在运行时,将方法调用优化掉以使运行时间为0。然而,这只有在JIT编译器启动后才会发生;这就是所谓的预热,在JVM上进行微基准测试时是必需的程序。

这是我运行的代码:

public static void timeLiteral() {
  for (int j = 0; j < 1_000_000_000; j++)
  {String s="hello World";}
}
public static void main(String... args) {
  for (int i = 0; i < 10; i++) {
    final long start = System.nanoTime();
    timeLiteral();
    System.out.println((System.nanoTime() - start) / 1000);
  }
}

以下是典型输出结果:

1412
38
25
1
1
0
0
1
0
1

您很快就可以观察到JIT的效果。

请注意,我在内部方法中不是迭代一千次,而是十亿次。


JIT非常快,不知道字符串字面量是否真的有必要提高性能..谢谢 - user1825817
如果您使用new String(),性能会大大降低,因为构造函数调用和随后的堆分配不会被优化掉。String s="hello World"基本上是一个无操作:将常量赋值给未使用的局部变量,因此对于HotSpot来说,这是一个微不足道的情况。 - Marko Topolnik
从未见过下划线符号的表示法。这在Java中允许吗? - Kirill Rakhman
@cypressious 自Java 7起已经允许。 - Marko Topolnik
@cypressious,对于像10亿这样的东西,你可以使用(int) 1e9 -> 在我看来更易读。 - bestsss

1

如已经被回答,第二个方法从字符串池中检索实例(记住字符串是不可变的)。

此外,您应该检查intern()方法,该方法使您能够将新的String()放入池中,以防您在运行时不知道字符串的常量值,例如:

String s = stringVar.intern();

or

new String(stringVar).intern();

我会添加额外的事实,你应该知道除了String对象之外,池中还存在更多信息(哈希码):这使得相关数据结构中的快速hashMap搜索字符串成为可能(而不是每次重新创建哈希码)。


0

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