Rechercher
 

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 :

Utilisation de StringBuffer ou de StringBuilder

Les chaînes de caractères Formatage de chaînes de caractères



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

La vidéo

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).


Utilisation de StringBuffer ou de StringBuilder

Concaténations non optimisées

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() );

    }
    
}
Exemple de concaténations de chaînes de caractères non optimisées

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" );
        
    }
    
}
Exemple de concaténations de chaînes de caractères non optimisées avec mesures de performances
la méthode System.currentTimeMillis() permet de récupérer le temps, au moment de l'appel, exprimé en milli-secondes.
la boucle permet de répéter le problème un certain nombre de fois (ici 100 000 fois) et donc d'obtenir un temps d'exécution non négligeable que l'on pourra comparer avec la prochaine version du code.

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
$> 

Utilisation de la classe StringBuilder

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" );
        
    }
    
}
Exemple de concaténations de chaînes de caractères via un StringBuilder

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
$> 

Différence entre StringBuilder et StringBuffer

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.

Optimisation des concaténations en ligne

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 );
                
    }
    
}
Exemple de concaténations en ligne

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
}
$> 
l'exécutable 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.
l'option -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.


Les chaînes de caractères Formatage de chaînes de caractères