Nous voulions le meilleur, mais il s'est avéré comme toujours.
Victor Chernomyrdin,
Homme d'État russe
Il y a des moments dans la vie où vous semblez tout faire correctement, mais quelque chose ne va pas.
Cette histoire concerne un de ces cas.
Une fois que j'ai regardé ce code et pensé à l'accélérer:
public String appendBounds(Data data) { int beginIndex = data.beginIndex; int endIndex = data.endIndex; return new StringBuilder() .append('L') .append(data.str, beginIndex, endIndex) .append(';') .toString(); }
Tout d'abord, je voulais calculer la longueur totale de la chaîne en utilisant les variables beginIndex
et endIndex
(ainsi que le fait qu'en plus de la chaîne tronquée, 2 caractères supplémentaires seront ajoutés à StringBuilder
) et transmettre cette valeur au constructeur StringBuilder
pour sélectionner immédiatement le tableau de la taille requise . Cette pensée me paraissait trop évidente, alors j'ai décidé d'essayer autre chose. Le fait que ce code n'ait pas été mis en évidence par l '«Idée» m'a incité à penser correctement, bien que cette fille intelligente suggère généralement de remplacer la chaîne courte de StringBuilder::append
par l'ajout de chaînes, qui est plus courte et plus facile à lire.
Un obstacle à cette simplification est l'utilisation de la StringBuilder.append(CharSequence, int, int)
. Étant donné que le champ data.str
est une chaîne, à l'aide de String.substring(beginIndex, endIndex)
vous pouvez en sélectionner une sous-chaîne et la transmettre à StringBuilder.append(String)
.
Code après conversion:
public String appendBounds(Data data) { int beginIndex = data.beginIndex; int endIndex = data.endIndex; String subString = data.str.substring(beginIndex, endIndex); return new StringBuilder() .append('L') .append(subString) .append(';') .toString(); }
Et maintenant, l'idée propose une simplification:
public String appendBounds(Data data) { int beginIndex = data.beginIndex; int endIndex = data.endIndex; return 'L' + data.str.substring(beginIndex, endIndex) + ';'; }
Cependant, notre objectif dans ce cas n'est pas tant la lisibilité que la productivité. Comparez les deux méthodes:
@BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Fork(jvmArgsAppend = {"-Xms2g", "-Xmx2g"}) public class StringBuilderAppendBenchmark { @Benchmark public String appendSubString(Data data) { String latinStr = data.latinStr; String nonLatinStr = data.nonLatinStr; int beginIndex = data.beginIndex; int endIndex = data.endIndex; String substring = data.nonLatin ? nonLatinStr.substring(beginIndex, endIndex) : latinStr.substring(beginIndex, endIndex); return new StringBuilder() .append('L') .append(substring) .append(';') .toString(); } @Benchmark public String appendBounds(Data data) { String latinStr = data.latinStr; String nonLatinStr = data.nonLatinStr; int beginIndex = data.beginIndex; int endIndex = data.endIndex; String appended = data.nonLatin ? nonLatinStr : latinStr; return new StringBuilder() .append('L') .append(appended, beginIndex, endIndex) .append(';') .toString(); } @State(Scope.Thread) public static class Data { String latinStr; String nonLatinStr; @Param({"true", "false"}) boolean nonLatin; @Param({"5", "10", "50", "100", "500", "1000"}) private int length; private int beginIndex; private int endIndex; private ThreadLocalRandom random = ThreadLocalRandom.current(); @Setup public void setup() { latinStr = randomString("abcdefghijklmnopqrstuvwxyz"); nonLatinStr = randomString(""); beginIndex = 1; endIndex = length + 1; } private String randomString(String alphabet) { char[] chars = alphabet.toCharArray(); StringBuilder sb = new StringBuilder(length + 2); for (int i = 0; i < length + 2; i++) { char c = chars[random.nextInt(chars.length)]; sb.append(c); } return sb.toString(); } } }
Le benchmark est aussi simple que deux sous: une chaîne aléatoire est ajoutée au StringBuilder
, dont la taille est déterminée par le champ de length
, et puisque le yard est 2019, vous devez le vérifier comme une chaîne contenant uniquement les caractères de l'alphabet latin principal (la ligne dite compressée, dans laquelle chaque caractère correspond à 1 octet) et une chaîne de caractères non latins (chaque caractère est représenté par 2 octets).
À un examen rapide, la méthode appendSubString
nous appendSubString
plus lente, car la quantité de données à coller coïncide avec celle de la méthode appendBounds
, cependant, dans la méthode appendSubString
, appendSubString
existe également une création explicite d'une sous-chaîne, c'est-à-dire l'allocation de mémoire pour un nouvel objet et la copie du contenu de data.latinStr
dedans / data.nonLatinStr
.
Le plus surprenant (mais seulement à première vue) les résultats de la mesure effectuée par moi en utilisant JDK11 sur une machine domestique (Intel Core i5-4690, 3,50 GHz) semblent être:
Benchmark nonLatin length Score Error Units appendBounds true 5 44,6 ± 0,4 ns/op appendBounds true 10 45,7 ± 0,7 ns/op appendBounds true 50 129,0 ± 0,5 ns/op appendBounds true 100 218,7 ± 0,8 ns/op appendBounds true 500 907,1 ± 5,5 ns/op appendBounds true 1000 1626,4 ± 13,0 ns/op appendSubString true 5 28,6 ± 0,2 ns/op appendSubString true 10 30,8 ± 0,2 ns/op appendSubString true 50 65,6 ± 0,4 ns/op appendSubString true 100 106,6 ± 0,6 ns/op appendSubString true 500 430,1 ± 2,4 ns/op appendSubString true 1000 839,1 ± 8,6 ns/op appendBounds:·gc.alloc.rate.norm true 5 184,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm true 10 200,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm true 50 688,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm true 100 1192,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm true 500 5192,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm true 1000 10200,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm true 5 136,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm true 10 160,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm true 50 360,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm true 100 608,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm true 500 2608,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm true 1000 5104,0 ± 0,0 B/op appendBounds false 5 20,8 ± 0,1 ns/op appendBounds false 10 24,0 ± 0,2 ns/op appendBounds false 50 66,4 ± 0,4 ns/op appendBounds false 100 111,0 ± 0,8 ns/op appendBounds false 500 419,2 ± 2,7 ns/op appendBounds false 1000 840,4 ± 7,8 ns/op appendSubString false 5 25,3 ± 0,3 ns/op appendSubString false 10 25,7 ± 0,2 ns/op appendSubString false 50 36,0 ± 0,1 ns/op appendSubString false 100 52,8 ± 0,4 ns/op appendSubString false 500 206,1 ± 6,1 ns/op appendSubString false 1000 388,1 ± 1,6 ns/op appendBounds:·gc.alloc.rate.norm false 5 80,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm false 10 88,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm false 50 320,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm false 100 544,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm false 500 2144,0 ± 0,0 B/op appendBounds:·gc.alloc.rate.norm false 1000 4152,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm false 5 96,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm false 10 112,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm false 50 192,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm false 100 288,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm false 500 1088,0 ± 0,0 B/op appendSubString:·gc.alloc.rate.norm false 1000 2088,0 ± 0,0 B/op
Réfutant notre hypothèse, la méthode appendSubString
dans la grande majorité des cas (y compris toujours pour les chaînes non latines) s'est avérée être plus rapide et moins gourmande (même si String::substring
renvoie un nouvel objet). Comment est-ce arrivé?
Je regarde dans le livre, je vois une figue
L'étude du code source de StringBuilder
aidera à StringBuilder
voile du secret. Les deux méthodes utilisées transmettent l'appel aux mêmes méthodes de AbstractStringBuilder
:
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, Comparable<StringBuilder>, CharSequence { @Override public StringBuilder append(String str) { super.append(str); return this; } @Override public StringBuilder append(CharSequence s, int start, int end) { super.append(s, start, end); return this; } }
Accédez à AbstractStringBuilder.append(String)
:
public AbstractStringBuilder append(String str) { if (str == null) { return appendNull(); } int len = str.length(); ensureCapacityInternal(count + len); putStringAt(count, str); count += len; return this; } private final void putStringAt(int index, String str) { if (getCoder() != str.coder()) { inflate(); } str.getBytes(value, index, coder); }
Qu'est-ce qui est intéressant ici? La AbstractStringBuilder::inflate
, comme son nom l'indique, étend le tableau AbstractStringBuilder.value
lors de la combinaison de chaînes différentes. Les données sont String::getBytes
dans la String::getBytes
:
void getBytes(byte[] dst, int dstBegin, byte coder) { if (coder() == coder) { System.arraycopy(value, 0, dst, dstBegin << coder, value.length); } else {
Qu'est-ce qui est important? Si les chaînes sont homogènes, alors le System::arraycopy
intrinsèque est utilisé pour déplacer les données, sinon StringLatin1::inflate
, qui par délégation nous conduit à la StringUTF16::inflate
:
Ainsi, si les lignes sont homogènes, la méthode System::arraycopy
dépendante de la plate-forme est utilisée pour déplacer les données, sinon une boucle (également intrinsèque) est utilisée. Cela signifie que lors du collage de deux lignes, où tous les caractères se trouvent dans l'ensemble de l'alphabet latin principal (en d'autres termes, tiennent sur 1 octet), les performances doivent être bien meilleures que lors du collage de lignes hétérogènes. Le benchmark le confirme (voir la sortie pour nonLatin = false
).
Maintenant, la méthode AbstractStringBuilder.append(CharSequence, int, int)
:
@Override public AbstractStringBuilder append(CharSequence s, int start, int end) { if (s == null) { s = "null"; } checkRange(start, end, s.length()); int len = end - start; ensureCapacityInternal(count + len); appendChars(s, start, end); return this; } private final void appendChars(CharSequence s, int off, int end) { if (isLatin1()) { byte[] val = this.value; for (int i = off, j = count; i < end; i++) { char c = s.charAt(i); if (StringLatin1.canEncode(c)) { val[j++] = (byte)c; } else { count = j; inflate(); StringUTF16.putCharsSB(this.value, j, s, i, end); count += end - i; return; } } } else { StringUTF16.putCharsSB(this.value, count, s, off, end); } count += end - off; }
Ici, l'approche est similaire à celle de l'exemple précédent: pour les chaînes homogènes, un mécanisme plus simple est utilisé (ici, copie de signe dans une boucle), pour les chaînes hétérogènes, nous utilisons StringUTF16
, cependant, notez que la StringUTF16::putCharsSB
appelée StringUTF16::putCharsSB
pas intrinsèque.
public static void putCharsSB(byte[] val, int index, CharSequence s, int off, int end) { checkBoundsBeginEnd(index, index + end - off, val); for (int i = off; i < end; i++) { putChar(val, index++, s.charAt(i)); } }
Ainsi, la structure interne des deux méthodes et la raison de leurs performances différentes sont plus ou moins claires pour nous. La question se pose naturellement - que faire des connaissances acquises ensuite? Il existe plusieurs options à la fois:
1) Gardez cela à l'esprit et quand il détecte un code suspect, changez-le avec vos mains
2) allez à Tagir et demandez-lui de déposer un chèque qui fera le travail à notre place
3) apportez des modifications au JDK afin que le code ne change pas du tout.
Bien sûr, nous commençons par le troisième. Prêt à tenter votre chance?
Abyss
Nous formerons sur les chats sur le code source du onzième Java, vous pouvez le télécharger ici .
La façon la plus simple et la plus évidente d'améliorer est de sélectionner une sous-chaîne déjà à l'intérieur de la AbstractStringBuilder.append(CharSequence, int, int)
:
Vous devez maintenant construire le JDK, exécuter les tests et exécuter le test StringBuilderAppendBenchmark::appendBounds
dessus, dont les résultats doivent être comparés avec les résultats du même test sur le JDK d'origine:
# before JDK, # after - Benchmark nonLatin length before after Units avgt true 5 44,6 64,4 ns/op avgt true 10 45,7 66,3 ns/op avgt true 50 129,0 168,9 ns/op avgt true 100 218,7 281,9 ns/op avgt true 500 907,1 1116,2 ns/op avgt true 1000 1626,4 2002,5 ns/op gc.alloc.rate.norm true 5 184,0 264,0 B/op gc.alloc.rate.norm true 10 200,0 296,0 B/op gc.alloc.rate.norm true 50 688,0 904,0 B/op gc.alloc.rate.norm true 100 1192,0 1552,0 B/op gc.alloc.rate.norm true 500 5192,0 6752,0 B/op gc.alloc.rate.norm true 1000 10200,0 13256,0 B/op avgt false 5 20,8 38,0 ns/op avgt false 10 24,0 37,8 ns/op avgt false 50 66,4 82,9 ns/op avgt false 100 111,0 138,8 ns/op avgt false 500 419,2 531,9 ns/op avgt false 1000 840,4 1002,7 ns/op gc.alloc.rate.norm false 5 80,0 152,0 B/op gc.alloc.rate.norm false 10 88,0 168,0 B/op gc.alloc.rate.norm false 50 320,0 440,0 B/op gc.alloc.rate.norm false 100 544,0 688,0 B/op gc.alloc.rate.norm false 500 2144,0 2688,0 B/op gc.alloc.rate.norm false 1000 4152,0 5192,0 B/op
Comment s'appelle, tout à coup! Non seulement des améliorations ne se sont pas produites, mais une détérioration s'est produite. Bon sang, mais comment?
Le fait est qu'au tout début, dans la description de la méthode StringBuilder::append
j'ai fait une petite omission, mais critique. La méthode a été décrite comme ceci:
public final class StringBuilder { @Override public StringBuilder append(String str) { super.append(str); return this; } }
Et voici sa vue complète:
public final class StringBuilder { @Override @HotSpotIntrinsicCandidate public StringBuilder append(String str) { super.append(str); return this; } }
Le code Java que nous avons examiné ci-dessus, chauffé et compilé au niveau C2, n'a pas d'importance, car il n'est pas exécuté, mais intrinsèque. Ceci est facile à prouver en supprimant le profil à l'aide de async-profiler . Ci-après, le profil est supprimé pour length = 1000
et nonLatin = true
:
# `appendSubString`, JDK ns percent samples top ---------- ------- ------- --- 19096340914 43.57% 1897673 jbyte_disjoint_arraycopy <--------- 13500185356 30.80% 1343343 jshort_disjoint_arraycopy <--------- 4124818581 9.41% 409533 java.lang.String.<init> # 2177311938 4.97% 216375 java.lang.StringUTF16.compress # 1557269661 3.55% 154253 java.util.Arrays.copyOfRange # 349344451 0.80% 34823 appendSubString_avgt_jmhStub 279803769 0.64% 27862 java.lang.StringUTF16.newString 274388920 0.63% 27312 org.openjdk.jmh.infra.Blackhole.consume 160962540 0.37% 15946 SpinPause 122418222 0.28% 11795 __memset_avx2
Le code de StringBuilder
(et AbstractStringBuilder
) ne sent même pas ici, près des 3/4 du profil sont occupés par un intrinsèque. Je voudrais observer à peu près la même image dans le profil de notre StringBuilder.append(CharSequence, int, int)
"amélioré" StringBuilder.append(CharSequence, int, int)
.
En fait, nous avons ceci:
ns percent samples top ---------- ------- ------- --- 19071221451 43.78% 1897827 jbyte_disjoint_arraycopy 6409223440 14.71% 638348 jlong_disjoint_arraycopy 3933622128 9.03% 387403 java.lang.StringUTF16.newBytesFor 2067248311 4.75% 204193 java.lang.AbstractStringBuilder.ensureCapacityInternal 1929218737 4.43% 194751 java.lang.StringUTF16.compress 1678321343 3.85% 166458 java.util.Arrays.copyOfRange 1621470408 3.72% 160849 java.lang.String.checkIndex 969180099 2.22% 96018 java.util.Arrays.copyOf 581600786 1.34% 57818 java.lang.AbstractStringBuilder.<init> 417818533 0.96% 41611 appendBounds_jmhTest 406565329 0.93% 40479 java.lang.String.<init> 340972882 0.78% 33727 java.lang.AbstractStringBuilder.append 299895915 0.69% 29982 java.lang.StringBuilder.toString 183885595 0.42% 18136 SpinPause 168666033 0.39% 16755 org.openjdk.jmh.infra.Blackhole.consume
Vous direz: "Les voici, intrinsèques, tout en haut!" En effet, seuls ceux-ci ne sont pas les mêmes intrinsèques (incl. Comparer le nom du second d'en haut). Rappel:
public final class StringBuilder { @Override @HotSpotIntrinsicCandidate public StringBuilder append(String str) { super.append(str); return this; } }
Ici, l'intrinsèque remplace l'appel à StringBuilder.append(String)
, mais dans notre patch, cet appel ne l'est pas! Appelé AbstractStringBuilder.append(String)
. L'appel jbyte_disjoint_arraycopy
nous jbyte_disjoint_arraycopy
est l'intrinsèque de StringLatin1::inflate
, appelé depuis AbstractStringBuider::putStringAt
via String::getBytes
. Autrement dit, contrairement à StringBuilder::append
traite non seulement le code spécifique à la plate-forme, mais aussi le code Java,
Compris la cause de l'échec, essayez de réussir autrement. Il est facile de deviner que nous devons en quelque sorte faire référence à StringBuilder::append
. Vous pouvez le faire en arrachant le correctif précédent et en apportant des modifications à StringBuilder
lui-même:
public final class StringBuilder {
Maintenant, tout est fait à bon escient: le StringBuilder :: append intrinsèque est appelé.
Reconstruisez, exécutez, comparez:
# before JDK, # after - Benchmark nonLatin length before after Units avgt true 5 44,6 60,2 ns/op avgt true 10 45,7 59,1 ns/op avgt true 50 129,0 164,6 ns/op avgt true 100 218,7 276,2 ns/op avgt true 500 907,1 1088,8 ns/op avgt true 1000 1626,4 1959,4 ns/op gc.alloc.rate.norm true 5 184,0 264,0 B/op gc.alloc.rate.norm true 10 200,0 296,0 B/op gc.alloc.rate.norm true 50 688,0 904,0 B/op gc.alloc.rate.norm true 100 1192,0 1552,0 B/op gc.alloc.rate.norm true 500 5192,0 6752,0 B/op gc.alloc.rate.norm true 1000 10200,0 13256,0 B/op avgt false 5 20,8 37,9 ns/op avgt false 10 24,0 37,9 ns/op avgt false 50 66,4 80,9 ns/op avgt false 100 111,0 125,6 ns/op avgt false 500 419,2 483,6 ns/op avgt false 1000 840,4 893,8 ns/op gc.alloc.rate.norm false 5 80,0 152,0 B/op gc.alloc.rate.norm false 10 88,0 168,0 B/op gc.alloc.rate.norm false 50 320,0 440,0 B/op gc.alloc.rate.norm false 100 544,0 688,0 B/op gc.alloc.rate.norm false 500 2144,0 2688,0 B/op gc.alloc.rate.norm false 1000 4152,0 5187,2 B/op
Je me sens vraiment très triste, mais ça ne s'est pas amélioré. Maintenant un nouveau profil:
ns percent samples top ---------- ------- ------- --- 19614374885 44.12% 1953620 jbyte_disjoint_arraycopy 6645299702 14.95% 662146 jlong_disjoint_arraycopy 4065789919 9.15% 400167 java.lang.StringUTF16.newBytesFor 2374627822 5.34% 234746 java.lang.AbstractStringBuilder.ensureCapacityInternal 1837858014 4.13% 183822 java.lang.StringUTF16.compress 1472039604 3.31% 145956 java.util.Arrays.copyOfRange 1316397864 2.96% 130747 appendBounds_jmhTest 956823151 2.15% 94959 java.util.Arrays.copyOf 573091712 1.29% 56933 java.lang.AbstractStringBuilder.<init> 434454076 0.98% 43202 java.lang.String.<init> 368480388 0.83% 36439 java.lang.AbstractStringBuilder.append 304409057 0.68% 30442 java.lang.StringBuilder.toString 272437989 0.61% 26833 SpinPause 201051696 0.45% 19985 java.lang.StringBuilder.<init> 198934052 0.45% 19810 appendBounds_avgt_jmhStub
Peu de choses ont changé. Pour moi, il n'est pas clair pourquoi l'intrinsèque n'a pas fonctionné lors de l'accès à StringBuilder.append(String)
partir de StringBuilder
. Il y a un soupçon que le collage (inlining) du corps de la méthode StringBuilder.append(String)
dans le corps de StringBuilder.append(CharSequence, int, int)
change quelque chose dans le traitement des appels de méthode VM.
Quoi qu'il en soit, c'est un fiasco, mon frère. Il n'a pas été possible de corriger le JDK, mais nous pouvons toujours effectuer le remplacement manuellement là où cela a du sens.
Retraite littéraire d'échecLe chiffrement de la réponse est arrivé en deux jours. Le navigateur ne veut pas se séparer d'Oto Velara, avec une entreprise qui construit des navires de guerre étonnamment rapides et puissants. Le navigateur ne veut pas me lire le cryptage. Il répète simplement la réponse du poste de commandement: "Non". Le chiffrement n'explique pas pourquoi «non». «Non» signifie en tout cas qu'il s'agit d'une personne connue d'un grand ordinateur. Si on ne savait rien de lui, la réponse serait oui: essayez-le. Dommage. C'est dommage de perdre une personne aussi intéressante. Et le commandant doit être désolé pour moi. Peut-être que la première fois est dommage. Il me voit déchirer les Vikings. Et il ne veut plus me pousser dans les lévriers.
Il est silencieux. Mais je sais qu'en fournissant une pénurie sauvage de travailleurs:
- Moi, camarade général, je travaille demain pour fournir. Lâchez-moi?
- Allez. - Et soudain elle sourit. "Vous savez, chaque nuage a une doublure argentée."
«Moi, camarade général, je suis toujours malade sans bien.»
"Et le voici." Il vous était interdit de le rencontrer, c'est mauvais. Mais aux trésors de notre expérience, nous avons ajouté un autre grain.
Conclusions:
- le code des méthodes JDK dans certains cas n'est pas lié à l'exécution réelle, car au lieu du corps de la méthode, un intrinsèque peut être exécuté, qui est caché dans les entrailles de la machine virtuelle.
- de telles méthodes peuvent être reconnues, en particulier, l'étiquette
@HotSpotIntrinsicCandidate
pointe vers elles, bien que certaines méthodes soient intrinsèques sans aucune indication, par exemple String::equals
(et beaucoup, beaucoup d'autres ). - La conclusion qui vient des deux premiers est que notre discussion sur le fonctionnement du code JDK peut être contraire à la réalité. C'est la vie
PS
Un autre remplacement possible:
StringBuilder sb = new StringBuilder(); sb.append(str, 0, endIndex);
PPS
Les développeurs Oracle soulignent à juste titre que
Il me semble assez étrange et surprenant d’introduire un chemin de code dans
sb.append (cs, int, int) qui alloue de la mémoire afin d'obtenir un intrinsèque qui
ne fait que parfois accélérer les choses. Comme vous l’avez observé, les performances
les compromis ne sont pas évidents.
Au lieu de cela, si nous voulons optimiser sb.append (cs, int, int), nous devrions peut-être simplement
avance et le faire, éventuellement en ajoutant ou en réorganisant les intrinsèques.
La solution proposée est l'intrinsification de StringBuilder.append(CharSequence, int, int)
.
→ Tâche
→ Discussion
PPS
Fait intéressant, en ce moment, lorsque vous écrivez quelque chose comme
StringBuilder sb = new StringBuilder(); sb.append(str.substring(0, endIndex));
"Idée" suggère de simplifier le code pour
StringBuilder sb = new StringBuilder(); sb.append(s, 0, endIndex);
Si les performances de cet endroit ne sont pas très importantes pour vous, il est probablement plus correct d'utiliser la deuxième version simplifiée. Pourtant, la plupart du code que nous écrivons est destiné à nos camarades, pas aux machines.