字符串串联:concat()与“ +”运算符


499

假设字符串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 2013年

1
使用javap来反汇编Java类文件。
热门点击

由于“不变性”,您可能应该使用StringBufferStringBuilder-(线程不安全,因此速度更快,取而代之的是
Ujjwal Singh 2015年

Answers:


560

不,不是。

首先,语义上略有不同。如果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,至少在性能方面,该方法是成功的。

Stringand 的源代码StringBuilder(及其包专用基类)在Sun JDK的src.zip中可用。您会看到正在建立一个char数组(根据需要调整大小),然后在创建final时将其丢弃String。实际上,内存分配出奇的快。

更新:正如Pawel Adamski指出的那样,在最近的HotSpot中,性能已经发生了变化。javac仍然产生完全相同的代码,但是字节码编译器作弊。简单的测试完全失败,因为整个代码主体都被丢弃了。总结System.identityHashCode(不是String.hashCode)表明StringBuffer代码有一点优势。在发布下一个更新时,或者使用其他JVM时,可能会发生更改。从@lukaseder热点JVM内部函数列表


4
@HyperLink您可以在使用该代码javap -c的已编译类上使用该代码。(哦,就像答案中的一样。您只需要解释字节码的反汇编,这应该没那么困难。)
Tom Hawtin-临时公告

1
您可以查阅JVM规范以了解各个字节码。您想参考的内容在第6章中。有点晦涩,但是您可以很容易地了解要点。
热门点击

1
我想知道为什么Java编译器StringBuilder即使在连接两个字符串时也使用它?如果String包含用于连接最多四个字符串或a中所有字符串的静态方法String[],则代码最多可以追加四个带有两个对象分配的字符串(结果String及其后备char[],两个都不冗余),以及任意数量的三个分配的字符串(的String[],结果String,和背衬char[],只有第一个是冗余的)。实际上,使用最多StringBuilder将需要四个分配,并且需要将每个字符复制两次。
supercat

该表达式a + = b。这不是说:a = a + b吗?
最受尊敬的先生

3
自从创建此答案以来,情况发生了变化。请阅读下面的答案。
帕维尔亚当斯基

90

Niyaz是正确的,但是也值得注意的是,特殊的+运算符可以通过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对象,而只需返回现有的对象即可。请参阅concat文档以确认这一点。

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


concat实际上并不能做到这一点。我已经用concat方法的反
汇编

10
实际上确实如此。查看您的concat代码的第一行。concat的问题在于,它始终会生成一个新的String()
Marcio Aguiar,

2
@MarcioAguiar:也许您的意思是+总是生成一个新的String-如您所说,concat当您连接一个empty时会有一个例外String
Blaisorblade 2014年

45

我运行了与@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次,每次运行10万次。结果如下:

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

我还没有反编译该类以查看内部结构或通过profiler运行它,但是我怀疑a += b花了很多时间创建的新对象,StringBuilder然后将它们转换回String


4
对象创建时间确实很重要。这就是为什么在许多情况下我们直接使用StringBuilder而不是利用+后面的StringBuilder的原因。
coolcfan 2012年

1
@coolcfan:当+用于两个字符串时,在任何情况下使用StringBuilder情况都比实际情况String.valueOf(s1).concat(s2)好吗?知道为什么编译器不会使用后者吗?[或者valueOfs1已知为非null的情况下忽略该调用]?
超级猫

1
@supercat对不起,我不知道。也许支持这种糖的人是回答这个问题的最佳人选。
coolcfan

25

大多数答案来自2008年。随着时间的流逝,情况似乎发生了变化。我使用JMH进行的最新基准测试表明,在Java 8上,Java的+速度大约是Java的两倍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永远不会通过串联a的元素来包含静态函数来形成字符串String[]。使用+这样的函数来连接8个字符串将需要构造并在以后放弃String[8],但这将是唯一需要被放弃构造的对象,而使用a则StringBuilder需要构造并放弃StringBuilder实例和至少一个char[]后备存储。
超级猫

@supercat String.join()在Java 8中添加了一些静态方法,作为围绕java.util.StringJoiner该类的快速语法包装器。
Ti Strga

@TiStrga:是否已+更改处理以使用此类功能?
超级猫

@supercat会破坏二进制向后兼容性,所以不行。这不过是在回答你的“为什么从来没有字符串包含静态函数”的评论:现在这样的功能。+不幸的是,您的其余建议(重构以使用它)将需要比Java开发人员愿意改变的更多的东西。
Ti Strga

@TiStrga:Java字节码文件是否可以通过在加载类的过程中解决的方式指示“如果函数X可用,则调用它;否则执行其他操作”。使用可以链接到Java的静态方法的静态方法生成代码,或者使用stringbuilder(如果无法使用该方法)似乎是最佳解决方案。
超级猫

22

Tom正确描述了+运算符的作用是正确的。它会创建一个临时文件StringBuilder,追加零件并以结尾toString()

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

@marcio:您已经创建了一个微基准;使用现代JVM,这不是配置代码的有效方法。

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

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


我想“这些临时操作”是指使用转义分析在可证明正确的堆栈上分配“堆”对象。尽管HotSpot中存在转义分析(用于删除某些同步),但在撰写本文时,我不相信它
Tom Hawtin-大头贴

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”代码总是导致相同的字符串,所以它不能优化“ a + b”代码中的字符串创建?它可以避免创建新的字符串。如果您不相信上述说法,请进行自我测试。


我在java jdk1.8.0_241上测试了您的代码,对我来说,“ a + b”代码给出了优化的结果。使用concat():203ms和使用“ +”:113ms。我猜在以前的版本中还没有进行优化。
Akki

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

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

所以,这些是+concat方法之间的基本区别。请享用 :)


亲爱的,您很清楚,任何字符串文字本身都被视为存储在字符串池中的字符串对象本身,因此在这种情况下,我们有4个字符串文字,因此显然应该在池中创建至少4个对象。
Deepak Sharma 2014年

1
我不这么认为:String s="I"+"am"+"good"+"boy"; String s2 = "go".concat("od"); System.out.println(s2 == s2.intern());prints true,这意味着"good"在调用之前不在字符串池中intern()
fabian 2014年

我只说这行String s =“ I” +“ am” +“ good” +“ boy”; 在这种情况下,所有4个字符串文字都保存在一个池中,因此应在池中创建4个对象。
Deepak Sharma 2014年

4

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

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

字符串串联的结果是对String对象的引用,该对象是两个操作数字符串的串联。在新创建的字符串中,左侧操作数的字符位于右侧操作数的字符之前。

除非表达式是常数表达式(第15.28节),否则将重新创建String对象(第12.5节)。

关于实现,JLS表示以下内容:

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

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

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


2

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

毗连运算符只能与字符串来完成。它检查数据类型的兼容性,如果不匹配则抛出错误。

除此之外,您提供的代码具有相同的功能。


2

我不这么认为。

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

我想这+=是使用+并类似优化实现的。


7
“当前+使用StringBuffer实现” False这是StringBuilder。StringBuffer是StringBuilder的线程安全暗示。
弗雷德里克·莫林

1
它以前是Java 1.5之前的StringBuffer,因为它是最初引入StringBuilder时的版本。
ccpizza

0

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

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


1
使用+运算符等效于使用StringBuilder(docs.oracle.com/javase/specs/jls/se8/html/…
ihebiheb,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.