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

时间:2008-09-06 16:08:55

标签: java string concatenation

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

11 个答案:

答案 0 :(得分:522)

不,不完全。

首先,语义略有不同。如果anull,则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中提供了StringStringBuilder(及其包私有基类)的源代码。您可以看到正在构建一个char数组(根据需要调整大小),然后在创建最终String时将其丢弃。实际上,内存分配速度惊人。

更新:正如Pawel Adamski所说,最近的HotSpot中的性能发生了变化。 javac仍然生成完全相同的代码,但字节码编译器作弊。简单的测试完全失败,因为整个代码都被抛弃了。汇总System.identityHashCode(不是String.hashCode)表示StringBuffer代码略有优势。在下次更新发布时,或者您使用其他JVM时,可能会发生更改。来自@lukasedera 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);
  • {strong> 2500ms 中执行的"a + b"版本。
  • 1200ms 中执行的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方法之间存在两个重要区别。

  1. 如果您使用的是 concat 方法,那么只有在 + 运算符的情况下才能连接字符串,您还可以连接字符串任何数据类型。

    例如:

    String s = 10 + "Hello";
    

    在这种情况下,输出应为 10Hello

    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);
    

    这样它只会创建五个对象。

  3. 所以这些是 + 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(),速度都是不可接受的。