在for循环中的字符串连接。Java 9

13

如果我错了,请纠正我。在Java 8中,出于性能的原因,当使用"+"运算符连接多个字符串时,会调用StringBuffer。这解决了创建大量中间字符串对象和污染字符串池的问题。

那么Java 9呢?新增了一个被称为"Invokedynamic"的新功能,并添加了一个新类StringConcatFactory,更好地解决了这个问题。

String result = "";
List<String> list = Arrays.asList("a", "b", "c");
for (String n : list) {
 result+=n;
}

我的问题是:在这个循环中创建了多少个对象?是否有任何中间对象?我该如何验证?


3
值得一读:(http://vanillajava.blogspot.co.uk/2015/10/common-misconception-how-many-objects.html) - Andy Turner
9
无论是Java8还是Java9 - 如果需要在循环中连接字符串,请使用StringBuilder - Oleksandr Pyrohov
4
这篇文章的要点是:这个问题本身没有意义,因为可能创建的字符串数量要么比你想象的多得多,要么比你想象的少得多。 - Andy Turner
3
起初我真的想说这肯定不可能还是对的,但是后来我测试了一下……你是正确的。非常好的评论。 - Eugene
2
@Eugene 在你的答案中提到使用 J10 可能值得一提。实际基准测试而非猜测加一分。 - the8472
显示剩余6条评论
3个回答

12

记录一下,这里是一个JMH测试...

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@State(Scope.Thread)
public class LoopTest {

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder().include(LoopTest.class.getSimpleName())
                .jvmArgs("-ea", "-Xms10000m", "-Xmx10000m")
                .shouldFailOnError(true)
                .build();
        new Runner(opt).run();
    }

    @Param(value = {"1000", "10000", "100000"})
    int howmany;

    @Fork(1)
    @Benchmark
    public String concatBuilder(){
        StringBuilder sb = new StringBuilder();
        for(int i=0;i<howmany;++i){
            sb.append(i);
        }
        return sb.toString();
    }

    @Fork(1)
    @Benchmark
    public String concatPlain(){
        String result = "";
        for(int i=0;i<howmany;++i){
            result +=i;
        }
        return result;
    }
}

产生的结果(仅在此处显示 100000)让我感到意外:

LoopTest.concatPlain       100000  avgt    5  3902.711 ± 67.215  ms/op
LoopTest.concatBuilder     100000  avgt    5     1.850 ±  0.574  ms/op

4
@FedericoPeraltaSchaffner,这些是在10上的,但我刚刚运行了9,它跟同样的东西很接近。 - Eugene

9
我的问题是:在这个循环中创建了多少对象?有中间对象吗?我如何验证?
剧透:JVM不会试图省略循环中的中间对象 - 因此在使用纯连接时它们将被创建。
首先让我们看一下字节码。我使用@Eugene提供的性能测试,将它们编译为Java8和Java9。这是我们要比较的两种方法:
public String concatBuilder() {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < howmany; ++i) {
        sb.append(i);
    }
    return sb.toString();
}

public String concatPlain() {
    String result = "";
    for (int i = 0; i < howmany; ++i) {
        result = result + i;
    }
    return result;
}

我的Java版本如下:

java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

java version "9.0.4"
Java(TM) SE Runtime Environment (build 9.0.4+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode)

JMH版本为1.20

这是我从javap -c LoopTest.class获得的输出:

concatBuilder()方法明确利用StringBuilder,在Java8和Java9中看起来完全相同:

public java.lang.String concatBuilder();
Code:
   0: new           #17                 // class java/lang/StringBuilder
   3: dup
   4: invokespecial #18                 // Method java/lang/StringBuilder."<init>":()V
   7: astore_1
   8: iconst_0
   9: istore_2
  10: iload_2
  11: aload_0
  12: getfield      #19                 // Field howmany:I
  15: if_icmpge     30
  18: aload_1
  19: iload_2
  20: invokevirtual #20                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  23: pop
  24: iinc          2, 1
  27: goto          10
  30: aload_1
  31: invokevirtual #21                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  34: areturn

请注意,在循环内调用了StringBuilder.append,而在循环外调用了StringBuilder.toString。这很重要-这意味着不会创建任何中间对象。在java8字节码中有些不同:
Java8中的concatPlain()方法:
public java.lang.String concatPlain();
Code:
   0: ldc           #22                 // String
   2: astore_1
   3: iconst_0
   4: istore_2
   5: iload_2
   6: aload_0
   7: getfield      #19                 // Field howmany:I
  10: if_icmpge     38
  13: new           #17                 // class java/lang/StringBuilder
  16: dup
  17: invokespecial #18                 // Method java/lang/StringBuilder."<init>":()V
  20: aload_1
  21: invokevirtual #23                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  24: iload_2
  25: invokevirtual #20                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  28: invokevirtual #21                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  31: astore_1
  32: iinc          2, 1
  35: goto          5
  38: aload_1
  39: areturn

你可以看到,在Java8中,StringBuilder.appendStringBuilder.toString都在循环语句内部被调用,这意味着它甚至不尝试省略中间对象的创建!代码如下所示:
public String concatPlain() {
    String result = "";
    for (int i = 0; i < howmany; ++i) {
        result = result + i;
        result = new StringBuilder().append(result).append(i).toString();
    }
    return result;
}

这里解释了concatPlain()concatBuilder()之间的性能差异(后者比前者快几千倍!)。在Java9中也存在同样的问题 - 它不会尝试避免循环内部的中间对象,但是它在循环内部做得比Java8略好(添加了性能结果)。
Java9中的concatPlain()方法:
public java.lang.String concatPlain();
Code:
   0: ldc           #22                 // String
   2: astore_1
   3: iconst_0
   4: istore_2
   5: iload_2
   6: aload_0
   7: getfield      #19                 // Field howmany:I
  10: if_icmpge     27
  13: aload_1
  14: iload_2
  15: invokedynamic #23,  0             // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
  20: astore_1
  21: iinc          2, 1
  24: goto          5
  27: aload_1
  28: areturn

这里是性能结果:

JAVA 8:

# Run complete. Total time: 00:02:18

Benchmark               (howmany)  Mode  Cnt     Score      Error  Units
LoopTest.concatBuilder     100000  avgt    5     2.098 ±    0.027  ms/op
LoopTest.concatPlain       100000  avgt    5  6908.737 ± 1227.681  ms/op

JAVA 9:

对于Java 9,有不同的策略定义为-Djava.lang.invoke.stringConcat。我尝试了所有这些策略:

默认值(MH_INLINE_SIZED_EXACT):

# Run complete. Total time: 00:02:30
Benchmark               (howmany)  Mode  Cnt     Score    Error  Units
LoopTest.concatBuilder     100000  avgt    5     1.625 ±  0.015  ms/op
LoopTest.concatPlain       100000  avgt    5  4812.022 ± 73.453  ms/op

-Djava.lang.invoke.stringConcat=BC_SB

# Run complete. Total time: 00:02:28
Benchmark               (howmany)  Mode  Cnt     Score    Error  Units
LoopTest.concatBuilder     100000  avgt    5     1.501 ±  0.024  ms/op
LoopTest.concatPlain       100000  avgt    5  4803.543 ± 53.825  ms/op

-Djava.lang.invoke.stringConcat=BC_SB_SIZED

# Run complete. Total time: 00:02:17
Benchmark               (howmany)  Mode  Cnt     Score     Error  Units
LoopTest.concatBuilder     100000  avgt    5     1.546 ±   0.027  ms/op
LoopTest.concatPlain       100000  avgt    5  4941.226 ± 422.704  ms/op

-Djava.lang.invoke.stringConcat=BC_SB_SIZED_EXACT

# Run complete. Total time: 00:02:45
Benchmark               (howmany)  Mode  Cnt      Score     Error  Units
LoopTest.concatBuilder     100000  avgt    5      1.560 ±   0.073  ms/op
LoopTest.concatPlain       100000  avgt    5  11390.665 ± 232.269  ms/op

-Djava.lang.invoke.stringConcat=BC_SB_SIZED_EXACT

# Run complete. Total time: 00:02:16
Benchmark               (howmany)  Mode  Cnt     Score     Error  Units
LoopTest.concatBuilder     100000  avgt    5     1.616 ±   0.030  ms/op
LoopTest.concatPlain       100000  avgt    5  8524.200 ± 219.499  ms/op

-Djava.lang.invoke.stringConcat=MH_SB_SIZED_EXACT

# Run complete. Total time: 00:02:17
Benchmark               (howmany)  Mode  Cnt     Score     Error  Units
LoopTest.concatBuilder     100000  avgt    5     1.633 ±   0.058  ms/op
LoopTest.concatPlain       100000  avgt    5  8499.228 ± 972.832  ms/op

-Djava.lang.invoke.stringConcat=MH_INLINE_SIZED_EXACT(是默认值,但我决定明确设置以便于实验的清晰度)

# Run complete. Total time: 00:02:23
Benchmark               (howmany)  Mode  Cnt     Score    Error  Units
LoopTest.concatBuilder     100000  avgt    5     1.654 ±  0.015  ms/op
LoopTest.concatPlain       100000  avgt    5  4812.231 ± 54.061  ms/op

我决定调查内存使用情况,但除了Java9消耗更多内存外,没有发现任何有趣的东西。如果有人感兴趣,可以参考附带的截图。当然,这些截图是在实际性能测量之后制作的,而不是在测试期间制作的。
Java8 concatBuilder(): Java8 concatBuilder() Java8 concatPlain(): enter image description here Java9 concatBuilder(): enter image description here Java9 concatPlain(): enter image description here

所以,回答你的问题,我可以说无论是Java8还是Java9都无法避免在循环中创建中间对象。

更新:

正如@Eugene所指出的那样,裸字节码可能没有意义,因为JIT在运行时进行了许多优化,这对我来说看起来是合理的,因此我决定添加由JIT优化的代码输出(通过-XX:CompileCommand=print,*LoopTest.concatPlain捕获)。

JAVA 8:

0x00007f8c2d216d29: callq   0x7f8c2d0fdea0    ; OopMap{rsi=Oop [96]=Oop off=1550}
                                            ;*synchronization entry
                                            ; - org.sample.LoopTest::concatPlain@-1 (line 73)
                                            ;   {runtime_call}
0x00007f8c2d216d2e: jmpq    0x7f8c2d216786
0x00007f8c2d216d33: mov     %rdx,%rdx
0x00007f8c2d216d36: callq   0x7f8c2d0fa1a0    ; OopMap{r9=Oop [96]=Oop off=1563}
                                            ;*new  ; - org.sample.LoopTest::concatPlain@13 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216d3b: jmpq    0x7f8c2d2167e6
0x00007f8c2d216d40: mov     %rbx,0x8(%rsp)
0x00007f8c2d216d45: movq    $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d4d: callq   0x7f8c2d0fdea0    ; OopMap{r9=Oop [96]=Oop rax=Oop off=1586}
                                            ;*synchronization entry
                                            ; - java.lang.StringBuilder::<init>@-1 (line 89)
                                            ; - org.sample.LoopTest::concatPlain@17 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216d52: jmpq    0x7f8c2d21682d
0x00007f8c2d216d57: mov     %rbx,0x8(%rsp)
0x00007f8c2d216d5c: movq    $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d64: callq   0x7f8c2d0fdea0    ; OopMap{r9=Oop [96]=Oop rax=Oop off=1609}
                                            ;*synchronization entry
                                            ; - java.lang.AbstractStringBuilder::<init>@-1 (line 67)
                                            ; - java.lang.StringBuilder::<init>@3 (line 89)
                                            ; - org.sample.LoopTest::concatPlain@17 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216d69: jmpq    0x7f8c2d216874
0x00007f8c2d216d6e: mov     %rbx,0x8(%rsp)
0x00007f8c2d216d73: movq    $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d7b: callq   0x7f8c2d0fdea0    ; OopMap{r9=Oop [96]=Oop rax=Oop off=1632}
                                            ;*synchronization entry
                                            ; - java.lang.Object::<init>@-1 (line 37)
                                            ; - java.lang.AbstractStringBuilder::<init>@1 (line 67)
                                            ; - java.lang.StringBuilder::<init>@3 (line 89)
                                            ; - org.sample.LoopTest::concatPlain@17 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216d80: jmpq    0x7f8c2d2168bb
0x00007f8c2d216d85: callq   0x7f8c2d0faa60    ; OopMap{r9=Oop [96]=Oop r13=Oop off=1642}
                                            ;*newarray
                                            ; - java.lang.AbstractStringBuilder::<init>@6 (line 68)
                                            ; - java.lang.StringBuilder::<init>@3 (line 89)
                                            ; - org.sample.LoopTest::concatPlain@17 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216d8a: jmpq    0x7f8c2d21693a
0x00007f8c2d216d8f: mov     %rdx,0x8(%rsp)
0x00007f8c2d216d94: movq    $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d9c: callq   0x7f8c2d0fdea0    ; OopMap{r9=Oop [96]=Oop r13=Oop off=1665}
                                            ;*synchronization entry
                                            ; - java.lang.StringBuilder::append@-1 (line 136)
                                            ; - org.sample.LoopTest::concatPlain@21 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216da1: jmpq    0x7f8c2d216a1c
0x00007f8c2d216da6: mov     %rdx,0x8(%rsp)
0x00007f8c2d216dab: movq    $0xffffffffffffffff,(%rsp)
0x00007f8c2d216db3: callq   0x7f8c2d0fdea0    ; OopMap{[80]=Oop [96]=Oop off=1688}
                                            ;*synchronization entry
                                            ; - java.lang.StringBuilder::append@-1 (line 208)
                                            ; - org.sample.LoopTest::concatPlain@25 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216db8: jmpq    0x7f8c2d216b08
0x00007f8c2d216dbd: mov     %rdx,0x8(%rsp)
0x00007f8c2d216dc2: movq    $0xffffffffffffffff,(%rsp)
0x00007f8c2d216dca: callq   0x7f8c2d0fdea0    ; OopMap{[80]=Oop [96]=Oop off=1711}
                                            ;*synchronization entry
                                            ; - java.lang.StringBuilder::toString@-1 (line 407)
                                            ; - org.sample.LoopTest::concatPlain@28 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216dcf: jmpq    0x7f8c2d216bf8
0x00007f8c2d216dd4: mov     %rdx,%rdx
0x00007f8c2d216dd7: callq   0x7f8c2d0fa1a0    ; OopMap{[80]=Oop [96]=Oop off=1724}
                                            ;*new  ; - java.lang.StringBuilder::toString@0 (line 407)
                                            ; - org.sample.LoopTest::concatPlain@28 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216ddc: jmpq    0x7f8c2d216c39
0x00007f8c2d216de1: mov     %rax,0x8(%rsp)
0x00007f8c2d216de6: movq    $0x23,(%rsp)
0x00007f8c2d216dee: callq   0x7f8c2d0fdea0    ; OopMap{[96]=Oop [104]=Oop off=1747}
                                            ;*goto
                                            ; - org.sample.LoopTest::concatPlain@35 (line 74)
                                            ;   {runtime_call}
0x00007f8c2d216df3: jmpq    0x7f8c2d216cae

正如您所看到的,StringBuilder::toString 在跳转之前被调用,这意味着所有操作都发生在循环内部。Java9 中的情况类似 - 在执行 goto 命令之前调用了 StringConcatHelper::newString
0x00007fa1256548a4: mov     %ebx,%r13d
0x00007fa1256548a7: sub     0xc(%rsp),%r13d   ;*isub {reexecute=0 rethrow=0 return_oop=0}
                                            ; - java.lang.StringConcatHelper::prepend@5 (line 329)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@16
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@172
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa1256548ac: test    %r13d,%r13d
0x00007fa1256548af: jl      0x7fa125654b11
0x00007fa1256548b5: mov     %r13d,%r10d
0x00007fa1256548b8: add     %r9d,%r10d
0x00007fa1256548bb: mov     0x20(%rsp),%r11d
0x00007fa1256548c0: cmp     %r10d,%r11d
0x00007fa1256548c3: jb      0x7fa125654b11    ;*invokestatic arraycopy {reexecute=0 rethrow=0 return_oop=0}
                                            ; - java.lang.String::getBytes@22 (line 2993)
                                            ; - java.lang.StringConcatHelper::prepend@11 (line 330)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@16
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@172
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa1256548c9: test    %r9d,%r9d
0x00007fa1256548cc: jbe     0x7fa1256548ef
0x00007fa1256548ce: movsxd  %r9d,%rdx
0x00007fa1256548d1: lea     (%r12,%r8,8),%r10  ;*getfield value {reexecute=0 rethrow=0 return_oop=0}
                                            ; - java.lang.String::length@1 (line 669)
                                            ; - java.lang.StringConcatHelper::mixLen@2 (line 116)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@11
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@105
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa1256548d5: lea     0x10(%r12,%r8,8),%rdi
0x00007fa1256548da: mov     %rcx,%r10
0x00007fa1256548dd: lea     0x10(%rcx,%r13),%rsi
0x00007fa1256548e2: movabs  $0x7fa11db9d640,%r10
0x00007fa1256548ec: callq   %r10              ;*invokestatic arraycopy {reexecute=0 rethrow=0 return_oop=0}
                                            ; - java.lang.String::getBytes@22 (line 2993)
                                            ; - java.lang.StringConcatHelper::prepend@11 (line 330)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@16
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@172
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa1256548ef: cmp     0xc(%rsp),%ebx
0x00007fa1256548f3: jne     0x7fa125654cb9    ;*ifeq {reexecute=0 rethrow=0 return_oop=0}
                                            ; - java.lang.StringConcatHelper::newString@1 (line 343)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@14
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@194
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa1256548f9: mov     0x60(%r15),%rax
0x00007fa1256548fd: mov     %rax,%r10
0x00007fa125654900: add     $0x18,%r10
0x00007fa125654904: cmp     0x70(%r15),%r10
0x00007fa125654908: jnb     0x7fa125654aa5
0x00007fa12565490e: mov     %r10,0x60(%r15)
0x00007fa125654912: prefetchnta 0x100(%r10)
0x00007fa12565491a: mov     0x18(%rsp),%rsi
0x00007fa12565491f: mov     0xb0(%rsi),%r10
0x00007fa125654926: mov     %r10,(%rax)
0x00007fa125654929: movl    $0xf80002da,0x8(%rax)  ;   {metadata('java/lang/String')}
0x00007fa125654930: mov     %r12d,0xc(%rax)
0x00007fa125654934: mov     %r12,0x10(%rax)   ;*new {reexecute=0 rethrow=0 return_oop=0}
                                            ; - java.lang.StringConcatHelper::newString@36 (line 346)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@14
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@194
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa125654938: mov     0x30(%rsp),%r10
0x00007fa12565493d: shr     $0x3,%r10
0x00007fa125654941: mov     %r10d,0xc(%rax)   ;*synchronization entry
                                            ; - java.lang.StringConcatHelper::newString@-1 (line 343)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@14
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@194
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa125654945: mov     0x8(%rsp),%ebx
0x00007fa125654949: incl    %ebx              ; ImmutableOopMap{rax=Oop [0]=Oop }
                                            ;*goto {reexecute=1 rethrow=0 return_oop=0}
                                            ; - org.sample.LoopTest::concatPlain@24 (line 74)

0x00007fa12565494b: test    %eax,0x1a8996af(%rip)  ;*goto {reexecute=0 rethrow=0 return_oop=0}
                                            ; - org.sample.LoopTest::concatPlain@24 (line 74)
                                            ;   {poll}

@D2k说实话,无论在字节码级别上显示什么都有点不相关,所有的优化都是由JIT完成的。 - Eugene
2
@Eugene,我已经添加了JIT生成的输出,看起来与字节码相比没有任何变化。 - Danylo Zatorsky

0

你的循环每次都会创建一个新的字符串。使用StringBuilder(而不是同步的StringBuffer,不应该使用)可以避免每次实例化一个新对象。

Java 9可能会添加新功能,但如果事情没有改变,我会感到惊讶。这个问题比Java 8还要旧。

补充:

Java 9已经修改了在单个语句中使用“+”运算符进行字符串连接的方式。在Java 8之前,它使用了一个构建器。现在,它使用了一种更高效的方法。然而,这并不能解决在循环中使用“+=”的问题。


你可以尝试自己运行并检查字节码。根本没有对StringBuilder的调用。谢谢。 - D2k
1
@D2k 或者你可以通过提供一个字节码转储来证明你的发现,从而完成你的帖子。我们在这里是来帮助你的,请不要让我们费尽周折。谢谢… - Dioxin
3
那么,你会感到惊讶的。这正是Java 9所发生的事情。在新功能和更改中,字符串连接实现已经改变,正如这个答案所述。 - fps
@VinceEmigh 你可以在评论区往下看。谢谢。 - D2k
1
@FedericoPeraltaSchaffner 谢谢伙计 - D2k
@FedericoPeraltaSchaffner 我真的期望在循环方面进行优化... :( - Eugene

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