Accès rapide :
La vidéo
Concaténations non optimisées
Utilisation de la classe StringBuilder
Différence entre StringBuilder et StringBuffer
Optimisation des concaténations en ligne
En fonction de comment vous allez réaliser des concaténations de chaînes de caractères, cela peut vous coûter très cher en temps CPU. Cette vidéo vous montre comment correctement réaliser des concaténations de chaînes de caractères en utilisant soit la classe StringBuffer, soit la classe StringBuilder (en fonction de si vous êtes thread-safe ou non).
Analysons le code suivant : il cherche à réaliser des concaténations de chaînes de caractères afin de produire un tableau HTML. On peut imaginer que vous ayez besoin de ce code dans un développement de type application Web, pourquoi pas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class Demo { public static String produceHtmlTable() { String result = "<table>\n"; for( int row=0; row<10; row++ ) { result += "\t<tr>\n\t\t"; for( int column=0; column<10; column++ ) { result += "<td>"; result += row * column; result += "</td>"; } result += "\n\t</tr>\n"; } result += "</table>\n"; return result; } public static void main( String [] args ) { System.out.println( produceHtmlTable() ); } } |
Le résultat produit par ce code est le suivant : notez bien la taille non négligeable de la chaîne de caractères finale.
<table> <tr> <td>0</td><td>0</td><td>0</td><td>0</td><td>0</td><td>0</td><td>0</td><td>0</td><td>0</td><td>0</td> </tr> <tr> <td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td> </tr> <tr> <td>0</td><td>2</td><td>4</td><td>6</td><td>8</td><td>10</td><td>12</td><td>14</td><td>16</td><td>18</td> </tr> <tr> <td>0</td><td>3</td><td>6</td><td>9</td><td>12</td><td>15</td><td>18</td><td>21</td><td>24</td><td>27</td> </tr> <tr> <td>0</td><td>4</td><td>8</td><td>12</td><td>16</td><td>20</td><td>24</td><td>28</td><td>32</td><td>36</td> </tr> <tr> <td>0</td><td>5</td><td>10</td><td>15</td><td>20</td><td>25</td><td>30</td><td>35</td><td>40</td><td>45</td> </tr> <tr> <td>0</td><td>6</td><td>12</td><td>18</td><td>24</td><td>30</td><td>36</td><td>42</td><td>48</td><td>54</td> </tr> <tr> <td>0</td><td>7</td><td>14</td><td>21</td><td>28</td><td>35</td><td>42</td><td>49</td><td>56</td><td>63</td> </tr> <tr> <td>0</td><td>8</td><td>16</td><td>24</td><td>32</td><td>40</td><td>48</td><td>56</td><td>64</td><td>72</td> </tr> <tr> <td>0</td><td>9</td><td>18</td><td>27</td><td>36</td><td>45</td><td>54</td><td>63</td><td>72</td><td>81</td> </tr> </table>
La question qu'on doit maintenant se poser est de savoir si le programme précédent est optimisé ou non. Pour être franc, pas vraiment.
Le problème vient du fait qu'à chaque utilisation d'un result += xxx
on doit produire une nouvelle chaîne de caractères
dans laquelle on doit d'abord recopier le contenu de la chaîne précédente et ensuite y concaténer le nouveau bout de chaîne de caractères.
La nouvelle chaîne produite est alors stockée dans la variable result
et la précédente instance de chaînes devient
potentiellement apte à être libérée par le garbage collector.
Et ainsi de suite ... Donc, plus on avance dans les boucles et plus les recopies de chaînes de caractères vont coûter chères.
On modifie légèrement le programme afin de faire des prises de mesures de performances.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class Demo { private static final int LOOP_COUNT = 100_000; public static String produceHtmlTable() { String result = "<table>\n"; for( int row=0; row<10; row++ ) { result += "\t<tr>\n\t\t"; for( int column=0; column<10; column++ ) { result += "<td>"; result += row * column; result += "</td>"; } result += "\n\t</tr>\n"; } result += "</table>\n"; return result; } public static void main( String [] args ) { long begin = System.currentTimeMillis(); for( int i=0; i<LOOP_COUNT; i++ ) { produceHtmlTable(); } long end = System.currentTimeMillis(); System.out.println( "Duration " + (end - begin) + " ms" ); } } |
System.currentTimeMillis()
permet de récupérer le temps, au moment de l'appel, exprimé en milli-secondes.
Voici le résultat produit par ce programme. Bien entendu, le temps affiché est dépendant de la puissance de calcul de votre machine : ne soyez pas surpris si vous n'avez pas les mêmes temps de réponse. Ajustez le nombre de tours de boucles si nécessaire, mais conservez la même valeur du compteur dans les exemples qui suivront.
$> javac Demo.java $> java Demo Duration 2233 ms $>
La classe java.lang.StringBuilder
permet de concaténer des chaînes de caractères de manière optimisée. On alloue un gros bloc de mémoire
dès le début et on ajoute au fur et à mesure des caractères dans ce bloc. Les ajouts se font via les méthodes append
: il y en a plusieurs
(une par type de données standard), cela s'appelle de la surcharge de méthodes.
Bien entendu, à force d'ajouter des caractères dans le StringBuilder
, il se peut que vous le saturiez : dans ce cas, le buffer sera
automatiquement agrandi, mais pas systématiquement.
Voici le programme précédent remanié afin d'utiliser cette notion de StringBuilder
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class Demo { private static final int LOOP_COUNT = 100_000; public static String produceHtmlTable() { StringBuilder builder = new StringBuilder( "<table>\n" ); for( int row=0; row<10; row++ ) { builder.append( "\t<tr>\n\t\t" ); for( int column=0; column<10; column++ ) { builder.append( "<td>" ); builder.append( row * column ); builder.append( "</td>" ); } builder.append( "\n\t</tr>\n" ); } builder.append( "</table>\n" ); return builder.toString(); } public static void main( String [] args ) { long begin = System.currentTimeMillis(); for( int i=0; i<LOOP_COUNT; i++ ) { produceHtmlTable(); } long end = System.currentTimeMillis(); System.out.println( "Duration " + (end - begin) + " ms" ); } } |
Voici le résultat produit par cette nouvelle version du programme. Vous constaterez la réduction importante du temps d'exécution de ce dernier.
$> javac Demo.java $> java Demo Duration 401 ms $>
Si vous parcourez la Javadoc du SE (Standard Edition) vous pourrez constater qu'en fait il y a deux classes extrêmement proches :
java.lang.StringBuffer
(existe depuis la première version de Java) et java.lang.StringBuilder
(existe depuis la version
5.0 du Java SE). Elles exposent toutes les deux exactement les mêmes méthodes. Du coup, qu'elle est la différence entre ces deux classes ?
C'est une très bonne question.
La réponse se trouve dans la Javadoc : c'est une histoire de synchronisation en cas d'accès multi-threadés. Qu'est-ce que cela veut dire ?
Si votre buffer est utilisé par plusieurs threads (plusieurs fils d'exécutions) en parallèles, il faudra choisir une implémentation
synchronisée contre les accès concurrents, à savoir la classe java.lang.StringBuffer
.
Par contre, si un seul thread manipule votre buffer, on est alors dans un context « thread-safe », il faudra alors utiliser la classe
java.lang.StringBuilder
. Cela permettra d'éviter de poser des verrous de synchronisation et donc, les temps de réponse seront meilleurs.
Si votre buffer est déclaré sous forme de variable locale à une méthode, on est forcément dans ce cas : vous pouvez donc utiliser
java.lang.StringBuilder
, ce qui est notre cas dans l'exemple précédent.
Utiliser un java.lang.StringBuffer
, alors que cela n'est pas nécessaire aura juste pour effet de ralentir inutilement les temps
d'exécution de votre programme. Par contre, utiliser un java.lang.StringBuilder
dans un contexte non « thread-safe »
aboutira à produire des résultats incohérents. Il ne faut surtout pas se retrouver dans cette situation.
java.lang.StringBuilder
, consultez notre
documentation de l'API Java.
java.lang.StringBuffer
, consultez notre
documentation de l'API Java.
Analysez le premier programme. Une question vous vient peut-être à l'esprit : est-il mieux d'utiliser un java.lang.StringBuilder
dans
ce cas (le buffer serait déclaré sous forme de variable locale, donc l'implémentation de type java.lang.StringBuilder
s'imposerait) ???
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Demo { public static void main( String [] args ) { String part1 = "part1"; double part2 = 3.141592654; int part3 = 123; char part4 = '\u03c0'; String fullMessage = "Begin - " + part1 + " - " + part2 + " - " + part3 + " - " + part4 + " - END"; System.out.println( fullMessage ); } } |
La réponse est non. Dans les spécifications Java, il est dit que le compilateur doit optimiser les concaténations en ligne (au sein d'une même
expression) en utilisant une instance de java.lang.StringBuilder
. Pour vous en convaincre, voici le code machine (le contenu du fichier
.class) désassemblé via l'outil javap
: vous y repérerez, grâce aux commentaires, les utilisations de cette classe de buffer.
$> javap -c Demo Compiled from "Demo.java" public class Demo { public Demo(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String part1 2: astore_1 3: ldc2_w #3 // double 3.141592654d 6: dstore_2 7: bipush 123 9: istore 4 11: sipush 960 14: istore 5 16: new #5 // class java/lang/StringBuilder 19: dup 20: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 23: ldc #7 // String Begin - 25: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 28: aload_1 29: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 32: ldc #9 // String - 34: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 37: dload_2 38: invokevirtual #10 // Method java/lang/StringBuilder.append:(D)Ljava/lang/StringBuilder; 41: ldc #9 // String - 43: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 46: iload 4 48: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 51: ldc #9 // String - 53: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 56: iload 5 58: invokevirtual #12 // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder; 61: ldc #13 // String - END 63: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 66: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 69: astore 6 71: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream; 74: aload 6 76: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 79: return } $>
javap
est fourni par le JDK (Java Development Kit). Si vous n'y avait pas accès, veuillez mettre à jour
votre variable d'environnement PATH
comme indiqué sur le chapitre d'installation du Java SE.
-c
utilisée sur la commande javap
permet de demandé de désassemblage du code.
Sans cette option, seules les informations sur la structure de la classe vous seront retournées.
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :