551

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

12 回答 12

616

不,不完全是。

首先,语义上略有不同。如果anull,则a.concat(b)抛出 aNullPointerExceptiona+=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 数组(根据需要调整大小),然后在创建 final 时将其丢弃String。在实践中,内存分配速度惊人。

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

于 2008-09-06T16:25:37.163 回答
97

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

String a = b + c + d;

进入

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

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

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

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

于 2008-09-06T16:24:09.227 回答
49

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

于 2008-09-06T19:25:12.847 回答
31

这里的大多数答案都是从 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
于 2017-09-29T08:56:14.913 回答
23

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

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

@marcio:您已经创建了一个微基准;对于现代 JVM,这不是分析代码的有效方法。

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

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

于 2008-09-06T18:38:28.887 回答
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 内执行。
  • 在1200 毫秒a.concat(b)执行。

测试了几次。concat()版本执行平均花费了一半的时间。

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

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

为什么编译器不能优化“a + b”代码中的字符串创建,知道它总是产生相同的字符串?它可以避免创建新的字符串。如果您不相信上面的陈述,请自行测试。

于 2008-09-06T17:59:20.713 回答
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
    

    案例二:

    现在我将通过+运算符连接相同的字符串

    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方法之间的基本区别。享受 :)

于 2014-08-19T07:49:00.190 回答
5

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

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

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

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

关于实施,JLS 说如下:

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

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

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

于 2017-03-26T15:00:44.573 回答
3

我不这么认为。

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

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

于 2008-09-06T16:22:39.230 回答
2

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

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

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

于 2008-09-06T16:16:06.547 回答
1

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

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

于 2015-02-10T09:51:21.977 回答
0

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

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

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

于 2021-12-30T19:19:07.867 回答