假设字符串a和b:
a += b
a = a.concat(b)
引擎盖下,它们是一样的吗?
这里以concat反编译为参考。我希望能够反编译+
运算符以查看它的作用。
public String concat(String s) {
int i = s.length();
if (i == 0) {
return this;
}
else {
char ac[] = new char[count + i];
getChars(0, count, ac, 0);
s.getChars(0, i, ac, count);
return new String(0, count + i, ac);
}
}
答案 0 :(得分:522)
不,不完全。
首先,语义略有不同。如果a
为null
,则a.concat(b)
会引发NullPointerException
,但a+=b
会将a
的原始值视为null
}}。此外,concat()
方法仅接受String
值,而+
运算符将静默地将参数转换为String(对象使用toString()
方法)。因此concat()
方法接受的内容更为严格。
要了解问题,请编写一个包含a += b;
public class Concat {
String cat(String a, String b) {
a += b;
return a;
}
}
现在用javap -c
(包含在Sun JDK中)进行反汇编。您应该看到包含以下内容的列表:
java.lang.String cat(java.lang.String, java.lang.String);
Code:
0: new #2; //class java/lang/StringBuilder
3: dup
4: invokespecial #3; //Method java/lang/StringBuilder."<init>":()V
7: aload_1
8: invokevirtual #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
11: aload_2
12: invokevirtual #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokevirtual #5; //Method java/lang/StringBuilder.toString:()Ljava/lang/ String;
18: astore_1
19: aload_1
20: areturn
所以,a += b
相当于
a = new StringBuilder()
.append(a)
.append(b)
.toString();
concat
方法应该更快。但是,使用更多字符串,StringBuilder
方法至少在性能方面获胜。
Sun JDK的src.zip中提供了String
和StringBuilder
(及其包私有基类)的源代码。您可以看到正在构建一个char数组(根据需要调整大小),然后在创建最终String
时将其丢弃。实际上,内存分配速度惊人。
更新:正如Pawel Adamski所说,最近的HotSpot中的性能发生了变化。 javac
仍然生成完全相同的代码,但字节码编译器作弊。简单的测试完全失败,因为整个代码都被抛弃了。汇总System.identityHashCode
(不是String.hashCode
)表示StringBuffer
代码略有优势。在下次更新发布时,或者您使用其他JVM时,可能会发生更改。来自@lukaseder,a list of HotSpot JVM intrinsics。
答案 1 :(得分:86)
Niyaz是正确的,但值得注意的是,特殊的+运算符可以被Java编译器转换为更高效的东西。 Java有一个StringBuilder类,它表示一个非线程安全的可变String。执行一堆字符串连接时,Java编译器会静默转换
String a = b + c + d;
到
String a = new StringBuilder(b).append(c).append(d).toString();
对于大字符串来说效率要高得多。据我所知,使用concat方法时不会发生这种情况。
但是,在将空String连接到现有String时,concat方法更有效。在这种情况下,JVM不需要创建新的String对象,只需返回现有的对象即可。请参阅the concat documentation进行确认。
因此,如果您非常关注效率,那么在连接可能为空的字符串时应使用concat方法,否则使用+。但是,性能差异应该可以忽略不计,你可能不应该担心这一点。
答案 2 :(得分:44)
我运行了与@marcio类似的测试,但改为使用以下循环:
String c = a;
for (long i = 0; i < 100000L; i++) {
c = c.concat(b); // make sure javac cannot skip the loop
// using c += b for the alternative
}
为了好的衡量,我也投入了StringBuilder.append()
。每次测试运行10次,每次运行100k次。结果如下:
StringBuilder
赢得了胜利。对于大多数运行,时钟时间结果为0,最长运行时间为16ms。a += b
每次运行大约需要40000毫秒(40秒)。concat
每次运行只需要10000毫秒(10秒)。我还没有反编译该类以查看内部或通过探查器运行它,但我怀疑a += b
花费大部分时间创建StringBuilder
的新对象,然后将它们转换回{ {1}}。
答案 3 :(得分:23)
这里的大多数答案都是从2008年开始的。看起来事情已经发生了变化。我使用JMH创建的最新基准测试显示,在Java 8上+
的速度比concat
快两倍左右。
我的基准:
@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS)
public class StringConcatenation {
@org.openjdk.jmh.annotations.State(Scope.Thread)
public static class State2 {
public String a = "abc";
public String b = "xyz";
}
@org.openjdk.jmh.annotations.State(Scope.Thread)
public static class State3 {
public String a = "abc";
public String b = "xyz";
public String c = "123";
}
@org.openjdk.jmh.annotations.State(Scope.Thread)
public static class State4 {
public String a = "abc";
public String b = "xyz";
public String c = "123";
public String d = "!@#";
}
@Benchmark
public void plus_2(State2 state, Blackhole blackhole) {
blackhole.consume(state.a+state.b);
}
@Benchmark
public void plus_3(State3 state, Blackhole blackhole) {
blackhole.consume(state.a+state.b+state.c);
}
@Benchmark
public void plus_4(State4 state, Blackhole blackhole) {
blackhole.consume(state.a+state.b+state.c+state.d);
}
@Benchmark
public void stringbuilder_2(State2 state, Blackhole blackhole) {
blackhole.consume(new StringBuilder().append(state.a).append(state.b).toString());
}
@Benchmark
public void stringbuilder_3(State3 state, Blackhole blackhole) {
blackhole.consume(new StringBuilder().append(state.a).append(state.b).append(state.c).toString());
}
@Benchmark
public void stringbuilder_4(State4 state, Blackhole blackhole) {
blackhole.consume(new StringBuilder().append(state.a).append(state.b).append(state.c).append(state.d).toString());
}
@Benchmark
public void concat_2(State2 state, Blackhole blackhole) {
blackhole.consume(state.a.concat(state.b));
}
@Benchmark
public void concat_3(State3 state, Blackhole blackhole) {
blackhole.consume(state.a.concat(state.b.concat(state.c)));
}
@Benchmark
public void concat_4(State4 state, Blackhole blackhole) {
blackhole.consume(state.a.concat(state.b.concat(state.c.concat(state.d))));
}
}
结果:
Benchmark Mode Cnt Score Error Units
StringConcatenation.concat_2 thrpt 50 24908871.258 ± 1011269.986 ops/s
StringConcatenation.concat_3 thrpt 50 14228193.918 ± 466892.616 ops/s
StringConcatenation.concat_4 thrpt 50 9845069.776 ± 350532.591 ops/s
StringConcatenation.plus_2 thrpt 50 38999662.292 ± 8107397.316 ops/s
StringConcatenation.plus_3 thrpt 50 34985722.222 ± 5442660.250 ops/s
StringConcatenation.plus_4 thrpt 50 31910376.337 ± 2861001.162 ops/s
StringConcatenation.stringbuilder_2 thrpt 50 40472888.230 ± 9011210.632 ops/s
StringConcatenation.stringbuilder_3 thrpt 50 33902151.616 ± 5449026.680 ops/s
StringConcatenation.stringbuilder_4 thrpt 50 29220479.267 ± 3435315.681 ops/s
答案 4 :(得分:22)
Tom正确描述了+运算符的作用。它会创建一个临时StringBuilder
,附加部分,并以toString()
结束。
但是,到目前为止,所有答案都忽略了HotSpot运行时优化的效果。具体而言,这些临时操作被识别为通用模式,并在运行时被更高效的机器代码替换。
@marcio:你创建了一个micro-benchmark;使用现代JVM,这不是分析代码的有效方法。
运行时优化很重要的原因是,一旦HotSpot开始运行,代码中的许多差异(甚至包括对象创建)都会完全不同。确切知道的唯一方法是分析您的代码 in situ 。
最后,所有这些方法实际上都非常快。这可能是过早优化的情况。如果你有很多代码连接字符串的代码,那么获得最大速度的方法可能与你选择的运算符无关,而是与你正在使用的算法有关!
答案 5 :(得分:21)
一些简单的测试怎么样?使用下面的代码:
long start = System.currentTimeMillis();
String a = "a";
String b = "b";
for (int i = 0; i < 10000000; i++) { //ten million times
String c = a.concat(b);
}
long end = System.currentTimeMillis();
System.out.println(end - start);
"a + b"
版本。a.concat(b)
。多次测试。 concat()
版本的执行平均占用了一半的时间。
这个结果让我感到惊讶,因为concat()
方法总是创建一个新字符串(它返回一个“new String(result)
”。众所周知:
String a = new String("a") // more than 20 times slower than String a = "a"
为什么编译器不能在“a + b”代码中优化字符串创建,知道它总是产生相同的字符串?它可以避免新的字符串创建。 如果您不相信上述陈述,请测试您的自我。
答案 6 :(得分:6)
基本上,+和concat
方法之间存在两个重要区别。
如果您使用的是 concat 方法,那么只有在 + 运算符的情况下才能连接字符串,您还可以连接字符串任何数据类型。
例如:
String s = 10 + "Hello";
在这种情况下,输出应为 10Hello 。
String s = "I";
String s1 = s.concat("am").concat("good").concat("boy");
System.out.println(s1);
在上述情况下,您必须提供两个必填字符串。
+ 和 concat 之间的第二个主要区别是:
案例1: 假设我以这种方式用 concat 运算符连接相同的字符串
String s="I";
String s1=s.concat("am").concat("good").concat("boy");
System.out.println(s1);
在这种情况下,池中创建的对象总数为7,如下所示:
I
am
good
boy
Iam
Iamgood
Iamgoodboy
案例2:
现在我将通过 + 运算符
来连接相同的字符串String s="I"+"am"+"good"+"boy";
System.out.println(s);
在上述情况下,创建的对象总数仅为5。
实际上,当我们通过 + 运算符连接字符串时,它会维护一个StringBuffer类来执行如下相同的任务: -
StringBuffer sb = new StringBuffer("I");
sb.append("am");
sb.append("good");
sb.append("boy");
System.out.println(sb);
这样它只会创建五个对象。
所以这些是 + 和 concat 方法之间的基本区别。 享受:)
答案 7 :(得分:3)
为了完整起见,我想补充一点,'+'运算符的定义可以在JLS SE8 15.18.1中找到:
如果只有一个操作数表达式是String类型,则为string 转换(第5.1.11节)在另一个操作数上执行以产生a 运行时的字符串。
字符串连接的结果是对String对象的引用 这是两个操作数字符串的串联。那些角色 左手操作数的前面是右手的字符 新创建的字符串中的操作数。
除非表达式是a,否则新创建String对象(第12.5节) 常数表达式(§15.28)。
关于JLS的实施说明如下:
实现可以选择执行转换和连接 在一步中避免创建然后丢弃中间体 字符串对象。增加重复字符串的性能 连接,Java编译器可以使用StringBuffer类或a 类似的技术减少了中间String对象的数量 通过评估表达式创建的。
对于基本类型,实现也可以优化掉 通过直接从基元转换来创建包装器对象 键入字符串。
因此,从'Java编译器可以使用StringBuffer类或类似技术来减少'来判断,不同的编译器可以产生不同的字节码。
答案 8 :(得分:2)
+运算符可以在字符串和字符串,char,integer,double或float数据类型值之间工作。它只是在连接之前将值转换为字符串表示。
concat运算符只能在字符串上完成。它检查数据类型兼容性并抛出错误,如果它们不匹配。
除此之外,您提供的代码也是相同的。
答案 9 :(得分:2)
我不这么认为。
a.concat(b)
在String中实现,我认为自早期java机器以来,实现没有太大变化。 +
操作实现依赖于Java版本和编译器。目前使用StringBuffer
实施+
以尽快进行操作。也许在未来,这将改变。在早期版本的java +
中,对字符串的操作要慢得多,因为它产生了中间结果。
我猜+=
是使用+
实现的,并且进行了类似的优化。
答案 10 :(得分:0)
当使用+时,速度随着字符串长度的增加而减小,但是当使用concat时,速度更稳定,最好的选择是使用具有稳定速度的StringBuilder类来实现这一点。
我想你可以理解为什么。但是创建长字符串的最好方法是使用StringBuilder()和append(),速度都是不可接受的。