字符串连接:concat() 与“+”运算符
-
09-06-2019 - |
题
假设字符串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);
}
}
解决方案
不,不完全是。
首先,语义上略有不同。如果 a
是 null
, , 然后 a.concat(b)
抛出一个 NullPointerException
但 a+=b
将处理原始值 a
就好像它是 null
. 。此外, concat()
方法只接受 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
方法胜出,至少在性能方面。
的源代码 String
和 StringBuilder
(及其包私有基类)可在 Sun JDK 的 src.zip 中找到。您可以看到您正在构建一个 char 数组(根据需要调整大小),然后在创建最终的时将其丢弃 String
. 。实际上,内存分配速度快得惊人。
更新: 正如 Pawel Adamski 指出的那样,最近的 HotSpot 的性能发生了变化。 javac
仍然生成完全相同的代码,但字节码编译器会作弊。简单的测试完全失败,因为整个代码都被丢弃了。求和 System.identityHashCode
(不是 String.hashCode
)显示 StringBuffer
代码有一点优势。当下一个更新发布时,或者如果您使用不同的 JVM,可能会发生变化。从 @卢卡塞德, HotSpot JVM 内在函数列表.
其他提示
尼亚兹 是正确的,但还值得注意的是,特殊的 + 运算符可以被 Java 编译器转换为更有效的东西。Java 有一个 StringBuilder 类,它表示一个非线程安全的、可变的 String。当执行一堆字符串连接时,Java 编译器会默默地转换
String a = b + c + d;
进入
String a = new StringBuilder(b).append(c).append(d).toString();
对于大字符串来说,效率明显更高。据我所知,当您使用 concat 方法时不会发生这种情况。
但是,将空字符串连接到现有字符串时,concat 方法更有效。在这种情况下,JVM 不需要创建新的 String 对象,只需返回现有的 String 对象即可。看 连接文档 来确认这一点。
因此,如果您非常关心效率,那么在连接可能为空的字符串时应该使用 concat 方法,否则使用 + 。但是,性能差异应该可以忽略不计,您可能不必担心这一点。
我运行了与 @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,最长的运行时间为 16 毫秒。a += b
每次运行大约需要 40000 毫秒(40 秒)。concat
每次运行仅需要 10000 毫秒(10 秒)。
我还没有反编译该类以查看内部结构或通过探查器运行它,但我怀疑 a += b
花费大量时间创建新对象 StringBuilder
然后将它们转换回 String
.
这里的大多数答案都是 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
Tom 正确地描述了 + 运算符的作用。它创建了一个临时的 StringBuilder
, ,附加各部分,并以 toString()
.
然而,到目前为止所有的答案都忽略了 HotSpot 运行时优化的影响。具体来说,这些临时操作被识别为常见模式,并在运行时替换为更高效的机器代码。
@马西奥:您已经创建了一个 微基准;对于现代 JVM,这不是分析代码的有效方法。
运行时优化很重要的原因是,一旦 HotSpot 运行,代码中的许多差异(甚至包括对象创建)都会完全不同。唯一确定的方法是分析你的代码 就地.
最后,所有这些方法实际上都非常快。这可能是过早优化的情况。如果您的代码需要大量连接字符串,那么获得最大速度的方法可能与您选择的运算符无关,而与您使用的算法无关!
一些简单的测试怎么样?使用下面的代码:
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"
执行版本 2500毫秒. - 这
a.concat(b)
执行于 1200毫秒.
测试了几次。这 concat()
版本执行平均花费了一半的时间。
这个结果让我感到惊讶,因为 concat()
方法总是创建一个新字符串(它返回一个“new String(result)
”。众所周知:
String a = new String("a") // more than 20 times slower than String a = "a"
为什么编译器不能优化“a + b”代码中的字符串创建,因为知道它总是会产生相同的字符串?它可以避免创建新的字符串。如果您不相信上述说法,请自行测试。
基本上,+ 和 + 之间有两个重要的区别 concat
方法。
如果您正在使用 连接 方法那么你只能连接字符串,而在这种情况下 + 运算符,您还可以将字符串与任何数据类型连接起来。
例如:
String s = 10 + "Hello";
在这种情况下,输出应该是 10你好.
String s = "I"; String s1 = s.concat("am").concat("good").concat("boy"); System.out.println(s1);
在上述情况下,您必须强制提供两个字符串。
第二个也是主要的区别 + 和 连接 就是它:
情况1:假设我将相同的字符串连接起来 连接 运算符以这种方式
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);
这样它只会创建五个对象。
所以伙计们,这些是之间的基本区别 + 和 连接 方法。享受 :)
为了完整起见,我想补充一点,“+”运算符的定义可以在 捷LS SE8 15.18.1:
如果只有一个类型字符串的操作数表达式,则在另一个操作数上执行字符串转换(§5.1.11),以在运行时产生字符串。
字符串串联的结果是对两个操作数字符串的串联对象的引用。左手操作数的字符之前是新创建的字符串中右手操作数的字符。
除非表达式是恒定表达式(§15.28),否则字符串对象是新创建的(§12.5)。
关于实施,JLS 表示如下:
实现可以选择在一个步骤中执行转换和串联,以避免创建然后丢弃中间字符串对象。为了提高重复的字符串串联的性能,Java编译器可以使用StringBuffer类或类似的技术来减少通过评估表达式创建的中间字符串对象的数量。
对于原始类型,实现还可以通过直接从原始类型转换为字符串来优化包装对象的创建。
因此,从“Java编译器可能使用StringBuffer类或类似的技术来减少”来看,不同的编译器可能会产生不同的字节码。
这 + 运算符 可以在字符串和字符串、字符、整数、双精度或浮点数据类型值之间工作。它只是在连接之前将值转换为其字符串表示形式。
这 连接运算符 只能用弦来完成。它检查数据类型兼容性,如果不匹配则抛出错误。
除此之外,您提供的代码执行相同的操作。
我不这么认为。
a.concat(b)
是用 String 实现的,我认为自早期的 java 机器以来,实现并没有太大变化。这 +
操作实现取决于Java版本和编译器。现在 +
是使用实现的 StringBuffer
使操作尽可能快。也许在未来,这种情况会改变。在早期版本的 java 中 +
对字符串的操作要慢得多,因为它会产生中间结果。
我猜可能是 +=
是使用实现的 +
并进行类似的优化。
当使用 + 时,速度会随着字符串长度的增加而降低,但是当使用 concat 时,速度会更稳定,最好的选择是使用具有稳定速度的 StringBuilder 类来做到这一点。
我想你能理解为什么。但是创建长字符串的最佳方法是使用 StringBuilder() 和append(),这两种方法的速度都是不可接受的。