web-dev-qa-db-fra.com

Concaténation de chaînes dans une boucle for. Java 9

Corrigez-moi si j'ai tort, s'il-vous plait. En Java 8, pour des raisons de performances, lors de la concaténation de plusieurs chaînes par l'opérateur "+", StringBuffer a été appelé. Et le problème de la création d'un groupe d'objets chaîne intermédiaires et de la pollution du pool de chaînes a été "résolu".

Qu'en est-il de Java 9? Une nouvelle fonctionnalité a été ajoutée à Invokedynamic. Et une nouvelle classe qui résout le problème encore mieux, StringConcatFactory.

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

Ma question est: combien d'objets sont créés dans cette boucle? Y a-t-il des objets intermedier? Et comment puis-je vérifier cela? 

10
D2k

Pour mémoire, voici un test 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;
    }
}

Donne un résultat (uniquement pour 100000 montré ici) auquel je ne m'attendais pas vraiment:

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

Ma question est la suivante: combien d'objets sont créés dans cette boucle? Y a-t-il des objets intermédiaires? Comment puis-je vérifier cela?

Spoiler:

JVM n'essaie pas d'omettre les objets intermédiaires dans la boucle. Ils seront donc créés lors de l'utilisation de la concaténation en clair.

Jetons d'abord un coup d'oeil au bytecode. J'ai utilisé des tests de performances aimablement fournis par @Eugene, puis compilés pour Java8, puis pour Java9. Voici ces 2 méthodes que nous allons comparer:

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

Mes versions de Java sont les suivantes:

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)

La version de JMH est 1.20

Voici le résultat obtenu de javap -c LoopTest.class:

La méthode concatBuilder() qui utilise StringBuilder a explicitement la même apparence pour Java8 et 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

Notez que l'appel de StringBuilder.append se produit à l'intérieur de la boucle, alors que StringBuilder.toString est appelé à l'extérieur de celle-ci. Ceci est important - cela signifie qu'il n'y aura pas d'objets intermédiaires créés. En Java8 bytecode c'est un peu différent:

Méthode concatPlain() en Java8:

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

Vous pouvez voir que dans Java8, StringBuilder.append et StringBuilder.toString sont appelés à l'intérieur de l'instruction loop, ce qui signifie que il n'essaie même pas d'omettre la création d'objets intermédiaires! Il peut être décrit dans le code ci-dessous:

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

Ceci explique la différence de performance entre concatPlain() et concatBuilder() (qui est quelques milliers de fois (!)). Le même problème se produit avec Java9 - il n'essaie pas d'éviter les objets intermédiaires dans une boucle, mais il effectue un travail Légèrement supérieur à l'intérieur d'une boucle que ne le fait Java8 (des résultats de performance sont ajoutés):

Méthode 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

Voici les résultats de performance:

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:

Pour Java 9, différentes stratégies sont définies avec -Djava.lang.invoke.stringConcat. Je les ai tous essayés:

Par défaut ( 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 (oui, c'est celui par défaut, mais j'ai décidé de le définir explicitement pour la clarté de l'expérience)

# 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

J'ai décidé d'étudier l'utilisation de la mémoire, mais je n'ai rien trouvé d'intéressant, sauf que Java9 consomme plus de mémoire. Captures d'écran ci-joint au cas où n'importe qui serait intéressé. Bien sûr, elles ont été réalisées après les mesures de performances réelles, mais pas pendant celles-ci.

Java8 concatBuilder ():  Java8 concatBuilder() Java8 concatPlain ():  enter image description here Java9 concatBuilder ():  enter image description here Java9 concatPlain ():  enter image description here

Alors oui, en répondant à votre question, je peux dire que ni Java8 ni Java9 ne peuvent éviter de créer des objets intermédiaires dans une boucle.

UPDATE:

Comme l'a souligné @Eugene, le bytecode nu n'a pas de sens puisque JIT effectue de nombreuses optimisations à l'exécution qui me paraissent logiques. J'ai donc décidé d'ajouter le résultat de l'optimisé par le code JIT (capturé par -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

Comme vous pouvez le constater, StringBuilder::toString est appelé avant le goto, ce qui signifie que tout se passe dans la boucle. Situation similaire avec Java9 - StringConcatHelper::newString est appelé avant la commande goto.

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}
5
Danylo Zatorsky

Votre boucle crée une nouvelle chaîne à chaque fois. StringBuilder (et non StringBuffer, qui est synchronisé et ne doit pas être utilisé), évite d'instancier un nouvel objet à chaque fois.

Java 9 ajoute peut-être de nouvelles fonctionnalités, mais je serais surpris que les choses changent. Ce problème est beaucoup plus ancien que Java 8. 

Une addition:

Java 9 a modifié la manière dont la concaténation de chaînes est effectuée lors de l'utilisation de l'opérateur "+" dans une seule instruction. Jusqu'en Java 8, il utilisait un constructeur. Maintenant, il utilise une approche plus efficace. Cependant, cela n'aborde pas l'utilisation de "+ =" dans une boucle.

0
Steve11235