字符串连接:concat() vs "+"操作符

假设字符串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);
}
}
1063669 次浏览

+操作符可以在字符串、字符、整数、双精度或浮点数据类型值之间工作。它只是在连接之前将值转换为字符串表示形式。

concat运营商只能在字符串上执行。它检查数据类型兼容性,如果不匹配,则抛出错误。

除此之外,您提供的代码也做同样的事情。

我不这么想。

a.concat(b)是在String中实现的,我认为自早期java机器以来,该实现没有太大变化。+操作实现取决于Java版本和编译器。目前+是使用StringBuffer来实现的,以使操作尽可能快。也许在未来,这种情况会改变。在早期版本的java# EYZ1中,对字符串的操作要慢得多,因为它产生中间结果。

我猜+=是使用+实现的,并进行了类似的优化。

"是正确的,但同样值得注意的是,特殊的+运算符可以被Java编译器转换为更有效的东西。Java有一个StringBuilder类,它表示一个非线程安全的可变String。当执行一系列String连接时,Java编译器会静默地进行转换

String a = b + c + d;

String a = new StringBuilder(b).append(c).append(d).toString();

这对于大字符串来说效率更高。据我所知,使用concat方法时不会发生这种情况。

然而,concat方法在将空字符串连接到现有字符串时效率更高。在这种情况下,JVM不需要创建新的String对象,只需返回现有的String对象。请参见concat文档来确认这一点。

所以如果你非常关心效率,那么你应该在连接可能为空的字符串时使用concat方法,否则使用+。然而,性能差异应该可以忽略不计,您可能永远都不应该担心这一点。

不,不完全是。

首先,语义上略有不同。如果anull,则a.concat(b)抛出NullPointerException,但a+=b将把a的原始值视为null。此外,concat()方法只接受String值,而+操作符将无声地将参数转换为String(对对象使用null0方法)。因此,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方法胜出,至少在性能方面是这样。

StringStringBuilder的源代码(以及它的包私有基类)可以在Sun JDK的src.zip中找到。您可以看到您正在构建一个char数组(根据需要调整大小),然后在创建最终的String时将其丢弃。实际上,内存分配的速度快得惊人。

正如Pawel Adamski所指出的,在最近的HotSpot中,性能已经发生了变化。javac仍然生成完全相同的代码,但是字节码编译器作弊了。简单的测试完全失败,因为整个代码体都被丢弃了。求和System.identityHashCode(而不是String.hashCode)表明StringBuffer代码略有优势。可能会在发布下一次更新时更改,或者如果您使用不同的JVM。从@lukasederHotSpot JVM intrinsic的列表

做一些简单的测试怎么样?使用下面的代码:

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”代码中的字符串创建,知道它总是导致相同的字符串?它可以避免创建新的字符串。 如果你不相信上面的说法,自己测试一下。< / p >

Tom准确地描述了+运算符的作用。它创建一个临时的StringBuilder,追加部分,并以toString()结束。

然而,到目前为止,所有的答案都忽略了HotSpot运行时优化的影响。具体来说,这些临时操作被认为是一种公共模式,并在运行时被更有效的机器代码所取代。

@marcio:你创建了一个微型基准测试;在现代JVM中,这不是一种分析代码的有效方法。

运行时优化之所以重要,是因为一旦HotSpot开始运行,代码中的许多差异(甚至包括对象创建)就完全不同了。唯一确定的方法是分析代码原位

最后,所有这些方法实际上都非常快。这可能是一个过早优化的例子。如果您的代码连接了很多字符串,那么获得最大速度的方法可能与您选择的操作符无关,而是与您使用的算法有关!

我运行了类似于@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次,每次运行100,000次。以下是调查结果:

  • StringBuilder轻松获胜。大多数运行的时钟时间结果为0,最长的运行时间为16毫秒。
  • a += b每次运行大约需要40000ms (40s)。
  • concat每次运行只需要10000ms(10秒)。

我还没有反编译类来查看内部结构或通过分析器运行它,但我怀疑a += b花费了大量时间来创建StringBuilder的新对象,然后将它们转换回String

基本上,+和concat方法之间有两个重要的区别。

  1. 如果你正在使用concat方法,那么你只能连接字符串,而在+操作符的情况下,你也可以连接字符串与任何数据类型。

    例如:

    String s = 10 + "Hello";
    

    在本例中,输出应该是10个你好

    String s = "I";
    String s1 = s.concat("am").concat("good").concat("boy");
    System.out.println(s1);
    

    在上面的例子中,你必须提供两个必须的字符串

  2. +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);
    

    这样它将只创建5个对象

所以这些是+concat方法之间的基本区别。 享受:)< / p >

当使用+时,速度会随着字符串长度的增加而降低,但是当使用concat时,速度会更稳定,最好的选择是使用StringBuilder类,它具有稳定的速度。

我想你能理解为什么。但是创建长字符串的最好方法是使用StringBuilder()和append(),这两种速度都是不可接受的。

为了完整起见,我想补充一下,'+'操作符的定义可以在JLS se8 15.18.1中找到:

如果只有一个操作数表达式类型为String,则String 在另一个操作数上执行转换(§5.1.11)以生成一个

字符串连接的结果是对string对象的引用 这是两个操作数字符串的连接。的字符 在右操作数字符的前面 在新创建的字符串中的操作数

String对象是新创建的(§12.5),除非表达式是a 常量表达式(§15.28)

关于实现,JLS说了以下几点:

实现可以选择执行转换和连接 一步就可以避免创建和丢弃中间产物 字符串对象。增加重复字符串的性能 连接时,Java编译器可以使用StringBuffer类或 类似的技术来减少中间字符串对象的数量 由表达式求值创建。

对于基本类型,实现也可以优化掉 通过直接从原语转换来创建包装器对象 输入一个字符串

因此,从“Java编译器可能使用StringBuffer类或类似的技术来减少”判断,不同的编译器可以产生不同的字节码。

这里的大多数答案都来自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

注意,当s为空时,s.concat("hello");将导致NullPointereException。在Java中,+操作符的行为通常由左操作数决定:

# EYZ0

但是,字符串是个例外。如果任意一个操作数是String,则预期结果是String。这就是null被转换为"null"的原因,即使您可能期望的是RuntimeException