关于java:字符串连接:concat()vs“+”运算符

String concatenation: concat() vs “+” operator

假设字符串A和B:

1
2
a += b
a = a.concat(b)

在引擎盖下面,它们是一样的吗?

这里是concat解压作为参考。我希望能够对+操作符进行反编译,并了解它的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
    }
}


不,不完全是这样。

首先,语义学上有点不同。如果anull,那么a.concat(b)抛出NullPointerException,但a+=ba的原值视为null。此外,concat()方法只接受String值,而+运算符将静默地将参数转换为字符串(对对象使用toString()方法)。因此,concat()方法在接受什么方面更为严格。

要查看引擎盖下面,用a += b;编写一个简单的类

1
2
3
4
5
6
public class Concat {
    String cat(String a, String b) {
        a += b;
        return a;
    }
}

现在用javap -c拆解(包含在Sun JDK中)。您应该看到一个列表,其中包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
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相当于

1
2
3
4
a = new StringBuilder()
    .append(a)
    .append(b)
    .toString();

concat方法应该更快。但是,如果字符串更多,则StringBuilder方法至少在性能方面会获胜。

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

更新:正如PawelAdamski指出的,性能在最近的热点发生了变化。javac仍然产生完全相同的代码,但是字节码编译器欺骗了。简单的测试完全失败,因为整个代码体都被丢弃了。总结System.identityHashCode(不是String.hashCode)表明StringBuffer代码有一点优势。当下一个更新发布时,或者如果您使用不同的JVM,则可能发生更改。来自@lukaseder,热点JVM内部列表。


Niyaz是正确的,但是值得注意的是,特殊的+运算符可以被Java编译器转换成更高效的东西。Java有一个String Bu建器类,它表示一个非线程安全的、可变的字符串。当执行串串连接时,Java编译器悄悄地转换。

1
String a = b + c + d;

进入之内

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

对于大的字符串来说,这是非常有效的。据我所知,使用concat方法时不会发生这种情况。

但是,当将空字符串连接到现有字符串时,concat方法更有效。在这种情况下,JVM不需要创建新的字符串对象,只需返回现有的字符串对象即可。请参阅concat文档以确认这一点。

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


我做了一个类似于@marcio的测试,但是用了以下循环:

1
2
3
4
5
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
}

为了更好的衡量,我还投出了《江户记》1(7)。每次测试运行10次,每次运行10万次。结果如下:

  • 江户十一〔五〕赢了。大多数跑步的计时结果为0,最长的为16毫秒。
  • a += b每次运行大约需要40000ms(40s)。
  • concat每次运行只需要10000ms(10s)。

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


大多数答案来自2008年。看来事情已经随着时间的推移而改变了。我用JMH制作的最新基准显示,在Java 8上,EDOCX1的0度比EDCOX1的1倍快2倍。

我的基准:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@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))));
    }
}

结果:

1
2
3
4
5
6
7
8
9
10
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

汤姆准确地描述了+运算符的作用。它创建一个临时的StringBuilder,附加零件,并以toString()结束。

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

@马西奥:您已经创建了一个微基准;使用现代的JVM,这不是一个分析代码的有效方法。

运行时优化之所以重要,是因为一旦热点出现,代码中的许多差异——甚至包括对象创建——都是完全不同的。唯一可以确定的方法是对代码进行就地分析。

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
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)"。众所周知:

1
String a = new String("a") // more than 20 times slower than String a ="a"

为什么编译器不能优化"a+b"代码中的字符串创建,因为知道它总是产生相同的字符串?它可以避免创建新的字符串。如果你不相信上面的说法,那就测试一下你自己。


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

  • 如果使用concat方法,则只能连接字符串,而对于+运算符,还可以将字符串连接到任何数据类型。

    例如:

    1
    String s = 10 +"Hello";

    在这种情况下,输出应该是10hello。

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

    在上述情况下,必须提供两个字符串。

  • +和concat的第二个主要区别是:

    案例1:假设我用concat运算符以这种方式连接相同的字符串

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

    在这种情况下,池中创建的对象总数为7,如下所示:

    1
    2
    3
    4
    5
    6
    7
    I
    am
    good
    boy
    Iam
    Iamgood
    Iamgoodboy

    案例2:

    现在,我将通过+运算符具体化相同的字符串

    1
    2
    String s="I"+"am"+"good"+"boy";
    System.out.println(s);

    在上述情况下,创建的对象总数只有5个。

    实际上,当我们通过+运算符具体化字符串时,它会维护一个StringBuffer类来执行以下相同的任务:

    1
    2
    3
    4
    5
    StringBuffer sb = new StringBuffer("I");
    sb.append("am");
    sb.append("good");
    sb.append("boy");
    System.out.println(sb);

    这样,它将只创建五个对象。

  • 伙计们,这是+和concat方法的基本区别。享受:


    为了完整起见,我想补充一下,可以在JLS SE8 15.18.1中找到"+"运算符的定义:

    If only one operand expression is of type String, then string
    conversion (§5.1.11) is performed on the other operand to produce a
    string at run time.

    The result of string concatenation is a reference to a String object
    that is the concatenation of the two operand strings. The characters
    of the left-hand operand precede the characters of the right-hand
    operand in the newly created string.

    The String object is newly created (§12.5) unless the expression is a
    constant expression (§15.28).

    关于实施,JLS表示:

    An implementation may choose to perform conversion and concatenation
    in one step to avoid creating and then discarding an intermediate
    String object. To increase the performance of repeated string
    concatenation, a Java compiler may use the StringBuffer class or a
    similar technique to reduce the number of intermediate String objects
    that are created by evaluation of an expression.

    For primitive types, an implementation may also optimize away the
    creation of a wrapper object by converting directly from a primitive
    type to a string.

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


    我不这么认为。

    EDCOX1 0的实现是用String实现的,我认为自从早期Java机器以来,实现没有发生太大的变化。EDCOX1的1操作实现依赖于Java版本和编译器。目前,+是利用StringBuffer来实现的,以使操作尽可能快。也许在将来,这会改变。在早期版本的Java EDCOX1中,1个字符串上的操作在产生中间结果时要慢得多。

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


    +运算符可以在字符串和string、char、integer、double或float数据类型值之间工作。它只是在串联之前将值转换为字符串表示形式。

    concat运算符只能对字符串执行。它检查数据类型的兼容性,如果不匹配则抛出一个错误。

    除此之外,您提供的代码执行相同的操作。


    当使用+时,速度会随着字符串长度的增加而降低,但当使用concat时,速度会更稳定,最佳选择是使用具有稳定速度的StringBuilder类来实现这一点。

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