【问题标题】:String concatenation in a for loop. Java 9for 循环中的字符串连接。爪哇 9
【发布时间】:2018-09-02 15:44:44
【问题描述】:

如果我错了,请纠正我。在 Java 8 中,出于性能原因,当通过“+”运算符连接多个字符串时调用 StringBuffer。并且“解决”了创建一堆中间字符串对象和污染字符串池的问题。

Java 9 怎么样? Invokedynamic 添加了一个新功能。还有一个新类可以更好地解决这个问题,StringConcatFactory。

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

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

【问题讨论】:

  • 无论是Java8 还是Java9 - 如果您需要在循环内连接字符串,请使用StringBuilder
  • @D2k 这篇文章的重点是:这个问题毫无意义,因为要么创建的字符串比你想象的要多得多,要么少得多。
  • @Oleksandr 最初我真的很想说这不可能仍然正确,但后来我测试了......你是对的。优秀的评论
  • @Eugene Using J10 可能值得在您的回答中提及。 +1 实际基准而不是推测。

标签: java string concatenation java-9


【解决方案1】:

为了记录,这里有一个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

【讨论】:

  • @FedericoPeraltaSchaffner 不,这些是在 10 上,但只是跑了 9 并且它接近相同的东西
【解决方案2】:

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

剧透:

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 得到的输出:

使用StringBuilder 的方法concatBuilder() 在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 稍好(添加了性能结果):

方法concatPlain()Java9:

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 concatPlain(): Java9 concatBuilder(): Java9 concatPlain():

所以是的,回答你的问题我可以说 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 在 goto 之前被调用,这意味着一切都在循环内发生。与 java9 类似的情况 - 在 goto 命令之前调用 StringConcatHelper::newString

JAVA 9:

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 我添加了由 JIT 生成的输出,看起来与字节码相比没有任何变化。
【解决方案3】:

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

Java 9 可能会添加新功能,但如果事情发生了变化,我会感到惊讶。这个问题比 Java 8 早得多。

加法:

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

【讨论】:

  • 您可以尝试自己运行它并检查字节码。根本没有对 StringBuilder 的调用。谢谢
  • @D2k 或者您实际上可以通过包含一个字节码转储来证明您的发现来完成您的帖子。我们是来帮忙的,不要让我们跳槽。谢谢...
  • 那么惊讶吧。这正是 Java 9 发生的情况。在新功能和更改中,字符串连接实现已更改,如 this answer 所述。
  • @VinceEmigh 您可以在 cmets 中查看更低的位置。谢谢
  • @FedericoPeraltaSchaffner thnx mate
猜你喜欢
  • 1970-01-01
  • 2018-05-24
  • 2020-06-04
  • 2016-01-14
  • 2015-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多