字符串连接:concat() vs "+" 运算符

586

假设有字符串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);
    }
}

3
我不确定 + 是否可被反编译。 - Galen Nare
1
使用 javap 命令来反汇编一个 Java 类文件。 - Hot Licks
由于“不可变性”,您应该使用StringBufferStringBuilder(线程不安全但更快) - Ujjwal Singh
https://dzone.com/articles/concatenating-strings-in-java-9 - user9999
12个回答

646

不完全正确。

首先,语义上略有不同。如果anull,则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 方法在性能方面获胜,至少在这方面是如此。
StringStringBuilder(以及它的包私有基类)的源代码在 Sun JDK 的 src.zip 中可用。你可以看到你正在建立一个字符数组(根据需要调整大小),然后在创建最终的 String 时将其丢弃。实际上,内存分配非常快速。
更新:正如 Pawel Adamski 所指出的那样,在更近期的 HotSpot 中性能已经发生了变化。javac 仍然生成完全相同的代码,但是字节码编译器作弊了。简单的测试完全失败,因为整个代码体被抛弃了。对 System.identityHashCode(而不是 String.hashCode)求和显示 StringBuffer 代码具有轻微优势。在下一次更新发布时或者使用不同的 JVM 时可能会发生变化。来自@lukasederHotSpot JVM 内在函数列表

4
您可以使用javap -c命令查看使用它的已编译类的代码。(哦,就像答案里说的那样。您只需要解释字节码反汇编,这不应该很难。) - Tom Hawtin - tackline
1
你可以参考JVM规范来了解各个字节码。需要参考的内容在第6章中。虽然有点晦涩,但你可以相对容易地理解其要义。 - Hot Licks
1
我想知道为什么Java编译器即使连接两个字符串也使用StringBuilder?如果String包含静态方法来连接最多四个字符串或String[]中的所有字符串,代码可以通过两个对象分配(结果String和其支持的char[],没有一个是冗余的)附加最多四个字符串,并且任意数量的字符串需要三个分配(String[],结果String和支持的char[],只有第一个是冗余的)。就目前而言,使用StringBuilder至少需要四个分配,并且需要复制每个字符两次。 - supercat
这是您所说的强制类型转换吗?我处于初级水平。通常我使用“casting”一词来表示在int和double之间进行转换(截断)。 - most venerable sir
3
自从这个回答被创建以来,情况发生了变化。请阅读我下面的回答。 - Paweł Adamski
显示剩余8条评论

103

Niyaz是正确的,但值得注意的是Java编译器可以将特殊的+运算符转换为更高效的形式。Java有一个StringBuilder类,表示非线程安全、可变字符串。在执行一系列字符串连接操作时,Java编译器会自动将其转换为使用StringBuilder。

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

对于大字符串来说,使用StringBuilder类比使用String的加法操作符(+)更高效。据我所知,当您使用加法操作符时,不会发生这种情况。

但是,当将空字符串连接到现有字符串上时,使用加法操作符更有效率。在这种情况下,JVM不需要创建新的字符串对象,而是可以直接返回现有的字符串对象。请参阅concat文档以确认此内容。

因此,如果您非常关心效率,则应在连接可能为空的字符串时使用concat方法,在其他情况下则使用加法操作符。然而,性能差异应该可以忽略不计,您可能永远不必担心这个问题。


concat 实际上并不是这样做的。我已经编辑了我的帖子,附上了 concat 方法的反编译代码。 - shsteimer
10
确实如此。看一下你的连接代码的第一行。concat的问题在于它总是生成一个新的String()。 - Marcio Aguiar
2
@MarcioAguiar:也许你的意思是+总是生成一个新的String - 正如你所说,当你连接一个空的String时,concat有一个例外。 - Blaisorblade

52

我进行了与@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


4
对象创建时间真的很重要。这就是为什么在许多情况下,我们直接使用 StringBuilder 而不是利用 + 号背后的 StringBuilder 的原因。 - Dante WWWW
1
@coolcfan:当两个字符串使用+时,是否有任何情况下使用StringBuilder比使用String.valueOf(s1).concat(s2)更好?有没有想法为什么编译器不使用后者[或在s1已知为非空的情况下省略valueOf调用]? - supercat
1
@supercat 抱歉,我不知道。也许那些负责这个项目的人是最好回答这个问题的人。 - Dante WWWW
搜索:invokedynamic StringConcatFactory - neoexpert

38

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

我想知道为什么Java的String从未包含一个静态函数来通过连接String[]的元素来形成字符串。使用这样的函数连接8个字符串需要构建和放弃String[8],但那将是唯一需要构建和放弃的对象,而使用StringBuilder则需要构建和放弃StringBuilder实例和至少一个char[]后备存储器。 - supercat
1
@supercat 在Java 8中添加了一些静态的String.join()方法,作为java.util.StringJoiner类的快速语法包装器。 - Ti Strga
@TiStrga:处理+的方式是否已更改为使用此类函数? - supercat
1
你能告诉我为什么 +StringBuilder 快两倍吗? - NanoNova
3
自Java 9以来,事情又有所改变。请更新。 - Olivier Grégoire
显示剩余2条评论

22

Tom正确地描述了+运算符的作用,它创建了一个临时的StringBuilder对象,将其组合起来,并最终调用toString()方法。

但是,到目前为止,所有答案都忽略了HotSpot运行时优化的影响。具体来说,这些临时操作被识别为常见模式,并在运行时替换为更高效的机器码。

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

运行时优化很重要的原因是,许多这些代码差异——甚至包括对象创建——在HotSpot开始后完全不同。想要确定结果,唯一的方法是就地对代码进行分析。

最后,所有这些方法实际上都非常快速。这可能是过早优化的情况。如果您有大量连接字符串的代码,则获取最大速度的方法可能与您选择的运算符无关,而是取决于您使用的算法!


我猜你所说的“这些临时操作”是指使用逃逸分析来在堆栈上分配“堆”对象,这样做是可以证明正确性的。虽然逃逸分析存在于HotSpot中(可用于删除一些同步),但我不认为在写作时它已经是这样了。 - Tom Hawtin - tackline
1
虽然这个话题很有趣,但我认为提到“过早优化”是非常重要的。如果你不确定这段代码实际上占据了总计算时间的重要比例,就不要在这个问题上花太多时间! - Juh_

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"的版本执行时间为2500ms
  • a.concat(b)的版本执行时间为1200ms

测试了多次。平均而言,concat()版本的执行时间只有前者的一半。

这个结果让我很惊讶,因为concat()方法总是创建一个新的字符串(它返回一个"new String(result)")。众所周知:

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

为什么编译器不能优化"a + b"代码中字符串的创建,明知道它总是得到相同的字符串?这样可以避免创建新的字符串。 如果您不相信上述说法,请自行测试。


2
我在Java jdk1.8.0_241上测试了你的代码,对我来说,“a+b”代码提供了优化的结果。使用concat():203毫秒,使用“+”:113毫秒。我猜在之前的版本中它并没有被这样优化。 - Akki

6
基本上,+ 和 concat 方法之间存在两个重要的区别。
  1. If you are using the concat method then you would only be able to concatenate strings while in case of the + operator, you can also concatenate the string with any data type.

    For Example:

    String s = 10 + "Hello";
    

    In this case, the output should be 10Hello.

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

    In the above case you have to provide two strings mandatory.

  2. The second and main difference between + and concat is that:

    Case 1: Suppose I concat the same strings with concat operator in this way

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

    In this case total number of objects created in the pool are 7 like this:

    I
    am
    good
    boy
    Iam
    Iamgood
    Iamgoodboy
    

    Case 2:

    Now I am going to concatinate the same strings via + operator

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

    In the above case total number of objects created are only 5.

    Actually when we concatinate the strings via + operator then it maintains a StringBuffer class to perform the same task as follows:-

    StringBuffer sb = new StringBuffer("I");
    sb.append("am");
    sb.append("good");
    sb.append("boy");
    System.out.println(sb);
    

    In this way it will create only five objects.

大家好,这里是+concat方法之间的基本区别。祝你们愉快 :)


亲爱的,你很清楚任何字符串字面量都被视为一个String对象本身,存储在String池中。因此,在这种情况下,我们有4个字符串字面量。所以显然至少应该在池中创建4个对象。 - Deepak Sharma
2
我不这么认为:String s="I"+"am"+"good"+"boy"; String s2 = "go".concat("od"); System.out.println(s2 == s2.intern()); 输出 true,这意味着在调用 intern() 之前,字符串池中没有 "good" - fabian
我只是在谈论这一行代码:String s="I"+"am"+"good"+"boy"; 在这种情况下,所有4个字符串字面量都被保存在池中。因此,应该在池中创建4个对象。 - Deepak Sharma

5
出于完整性的考虑,我想补充一下'+'操作符的定义可以在JLS SE8 15.18.1中找到:

如果只有一个操作数表达式是字符串类型,则在运行时对另一个操作数执行字符串转换(§5.1.11)以生成字符串。

字符串连接的结果是指向包含两个操作数字符串连接的String对象的引用。左操作数的字符在新创建的字符串中位于右操作数之前。

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

关于JLS所述的实现方式如下:

为了避免创建和丢弃中间的String对象,实现可能选择在一步中执行转换和连接。为了提高重复字符串连接的性能,Java编译器可以使用StringBuffer类或类似技术来减少通过评估表达式创建的中间字符串对象的数量。

对于基本类型,实现还可以通过将基本类型直接转换为字符串来优化掉包装对象的创建。

因此,根据文中'a Java compiler may use the StringBuffer class or a similar technique to reduce'所述,不同的编译器可能会产生不同的字节码。

3
+ 运算符 可以在字符串和字符串、字符、整数、双精度或浮点数据类型值之间起作用。它只是在连接之前将值转换为其字符串表示形式。 concat 运算符 只能在字符串上进行,并检查数据类型的兼容性,如果不匹配,则会抛出错误。
除此之外,您提供的代码执行相同的操作。

3

我不这么认为。

a.concat(b) 是在 String 中实现的,我认为自从早期 Java 机器以来,该实现并没有太大变化。而 + 操作的实现取决于 Java 版本和编译器。目前,+ 使用StringBuffer 来尽可能地加速操作。也许在未来会有所改变。在早期版本的 Java 中,+ 在字符串上的操作速度要慢得多,因为它会产生中间结果。

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


7
目前使用的是StringBuilder,而不是StringBuffer。StringBuffer是StringBuilder的线程安全实现。 - Frederic Morin
1
在Java 1.5之前,它曾经是StringBuffer,因为那个版本是StringBuilder首次引入的版本。 - ccpizza
你说“当前”,指的是哪个JDK? - dzieciou

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