【问题标题】:String concatenation: concat() vs "+" operator字符串连接:concat() 与“+”运算符
【发布时间】:2010-09-08 00:53:13
【问题描述】:

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

【问题讨论】:

标签: java string concatenation


【解决方案1】:

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

System.out.println(3 + 'a'); //100

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

【讨论】:

    【解决方案2】:

    我不这么认为。

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

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

    【讨论】:

    • "目前+是使用StringBuffer实现的" False 是StringBuilder。 StringBuffer 是 StringBuilder 的线程安全实现。
    • java 1.5之前是StringBuffer,刚引入StringBuilder时就是那个版本。
    【解决方案3】:

    不,不完全是。

    首先,语义略有不同。如果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 中找到。您可以看到您正在构建一个 char 数组(根据需要调整大小),然后在创建最终的 String 时将其丢弃。在实践中,内存分配速度惊人。

    更新: 正如 Pawel Adamski 所说,最近的 HotSpot 的性能发生了变化。 javac 仍然产生完全相同的代码,但字节码编译器会作弊。简单的测试完全失败,因为整个代码体都被丢弃了。对System.identityHashCode 求和(不是String.hashCode)表明StringBuffer 代码有一点优势。下一次更新发布或您使用不同的 JVM 时可能会发生变化。来自@lukasedera list of HotSpot JVM intrinsics

    【讨论】:

    • @HyperLink 您可以在使用它的编译类上看到使用javap -c 的代码。 (哦,和答案一样。你只需要解释字节码反汇编,这应该不难。)
    • 您可以咨询JVM spec以了解各个字节码。你想参考的东西在第 6 章。有点晦涩,但你可以很容易地理解它的要点。
    • 我想知道为什么Java编译器即使在连接两个字符串时也使用StringBuilder?如果String 包含连接最多四个字符串的静态方法,或者String[] 中的所有字符串,则代码最多可以附加四个字符串并分配两个对象(结果String 及其支持char[],没有一个冗余),以及具有三个分配的任意数量的字符串(String[]、结果String和支持char[],只有第一个是冗余的)。事实上,使用StringBuilder充其量需要四次分配,并且需要复制每个字符两次。
    • 那个表达式,a+=b。不是说:a=a+b?
    • 自创建此答案以来,情况发生了变化。请在下面阅读我的答案。
    【解决方案4】:

    这里的大多数答案来自 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 则需要构造和放弃@987654330 @instance 和至少一个 char[] 后备存储。
    • @supercat 在 Java 8 中添加了一些静态 String.join() 方法,作为 java.util.StringJoiner 类的快速语法包装器。
    • @TiStrga:+ 的处理方式是否已更改为使用此类功能?
    • @supercat 这会破坏二进制向后兼容性,所以不会。它只是为了回复您的“为什么 String 从未包含静态函数”评论:现在有 is 这样的函数。遗憾的是,您的提案的其余部分(重构 + 以使用它)需要的不仅仅是 Java 开发人员愿意更改的内容。
    • 自 Java 9 以来情况再次发生变化。请更新。
    【解决方案5】:

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

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

    【讨论】:

    【解决方案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 个对象在游泳池里。
    • 我不这么认为:String s="I"+"am"+"good"+"boy"; String s2 = "go".concat("od"); System.out.println(s2 == s2.intern()); 打印出true,这意味着"good" 在调用intern() 之前不在字符串池中
    • 我只说这一行 String s="I"+"am"+"good"+"boy";在这种情况下,所有 4 个字符串文字都保存在一个池中。因此应该在池中创建 4 个对象。
    【解决方案7】:

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

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

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

    【讨论】:

      【解决方案8】:

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

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

      字符串连接的结果是一个String对象的引用 那是两个操作数字符串的连接。那些角色 左边的操作数在右边的字符之前 新创建的字符串中的操作数。

      字符串对象是新创建的(第 12.5 节),除非表达式是 常量表达式(第 15.28 节)。

      关于实现,JLS 说如下:

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

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

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

      【讨论】:

        【解决方案9】:

        Niyaz 是正确的,但还值得注意的是,特殊的 + 运算符可以通过 Java 编译器转换为更有效的东西。 Java 有一个 StringBuilder 类,它代表一个非线程安全的可变字符串。当执行一堆字符串连接时,Java 编译器会默默地转换

        String a = b + c + d;
        

        进入

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

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

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

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

        【讨论】:

        • concat 事实上并没有这样做。我用 concat 方法的反编译编辑了我的帖子
        • 事实上确实如此。查看您的 concat 代码的第一行。 concat 的问题是它总是生成一个 new String()
        • @MarcioAguiar:也许你的意思是 + 总是生成一个新的String - 正如你所说,concat 有一个例外,当你连接一个空的String
        【解决方案10】:

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

        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”代码中的字符串创建,知道它总是产生相同的字符串?它可以避免创建新的字符串。 如果您不相信上面的陈述,请自行测试。

        【讨论】:

        • 我在 java jdk1.8.0_241 上测试了你的代码,对我来说,“a+b”代码给出了优化的结果。使用 concat(): 203ms 和使用 "+": 113ms 。我猜在以前的版本中它并没有那么优化。
        【解决方案11】:

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

        【讨论】:

        • 对象创建时间真的很重要。这就是为什么在很多情况下我们直接使用 StringBuilder 而不是利用 + 后面的 StringBuilder。
        • @coolcfan: 当+ 用于两个字符串时,有没有使用StringBuilderString.valueOf(s1).concat(s2) 更好的情况?知道为什么编译器不使用后者[或者在已知s1 为非空的情况下省略valueOf 调用]?
        • @supercat 抱歉,我不知道。也许这些糖背后的人是回答这个问题的最佳人选。
        • 搜索:invokedynamic StringConcatFactory
        【解决方案12】:

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

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

        @marcio:你创建了一个micro-benchmark;对于现代 JVM,这不是分析代码的有效方法。

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

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

        【讨论】:

        • 我猜“这些临时操作”是指使用转义分析在堆栈上分配可证明正确的“堆”对象。虽然 HotSpot 中存在逃逸分析(对于删除一些同步很有用),但我不相信,在撰写本文时,你
        猜你喜欢
        • 1970-01-01
        • 2012-02-04
        • 2016-07-19
        • 1970-01-01
        • 2012-01-20
        • 2017-02-04
        • 2014-02-27
        • 2015-08-28
        • 1970-01-01
        相关资源
        最近更新 更多