Utilisation de StringBuffer et autres ruses
Par batmat le mercredi 9 juin 2004, 22:30 - Technique - Lien permanent
Mise à jour 2009 : Aujourd'hui, préférez simplement l'utilisation de la classe StringBuilder à la place de StringBuffer. Le reste de l'article reste valable.
Voici donc le premier billet traitant de programmation, je pense que ce n'est que le premier d'une longue série. J'espère aussi que ça sera le petit coup de pouce qui manque à Syl pour savoir sur quoi blogguer plus souvent :-).
Aujourd'hui on va faire du Java.
Exemple de code qu'il est MAL®
Vous le savez certainement ou vous l'avez au moins entendu dire : la classe String est non mutable. Qu'est-ce que ça signifie ? Ça veut dire qu'une fois instanciée vous ne pouvez plus la modifier... Vous pouvez vérifier, il n'existe aucun mutateur (setter) pour cette classe.
Exemple : écrivons une méthode qui ajoute à hello les chiffres 1 à 9.
public static String sayBadHello()
{
String bonjour = "bonjour";
for(int i=1;i<10;++i)
{
bonjour += i;
}
return bonjour;
}
Ici, qu'est-ce qui se passe ? On instancie un objet String et on y met "bonjour". Ensuite, on fait quoi ? Ben, à chaque boucle, on prend la variable bonjour, on lui concatène le chiffre en cours et on affecte le résultat à bonjour.
En faisant ça : on instancie une nouvelle String à chaque fois et l'ancienne String est garbagée (comme l'utilisateur-développeur ne possède alors plus aucune référence vers cette variable, la machine virtuelle sait qu'elle peut supprimer cet ancien objet dès qu'elle le voudra).
Vous voyez le problème maintenant ? Pour une boucle de n éléments, on va instancier n objets ! Si vous passez votre temps à appeler cette méthode, vous déclencherez autant de fois cette aberration.
StringBuffer
Pour éviter ces nombreuses instanciations inutiles, il existe la classe StringBuffer qui, elle, est mutable. J'ose espérer que vous les utilisez déjà et que pour les étudiants d'entre vous, vos profs ont pensé à insister dessus. S'ils ne connaissent pas, ben je m'inquiéterai pour votre formation. Si vous ne connaissiez pas, à partir de maintenant vous n'aurez plus d'excuse :-).
Utilisation des méthodes append
Ces méthodes permettent d'ajouter un grand nombre de types différents à la chaîne stockée sous forme de caractères.
Voici un exemple simple d'utilisation pour corriger l'exemple précédent :
public static String sayGoodHello()
{
StringBuffer bonjour = new StringBuffer(20);
bonjour.append("bonjour");
for(int i=1;i<10;++i)
{
bonjour.append(i);
}
return bonjour.toString();
}
Plus de finesse
Quelques petits compléments pour les plus affamés :-).
-
Les plus attentifs auront remarqué le ++i là où on rencontre le plus souvent un i++. Ce n'est pas anodin, c'est juste un léger gain en terme de performances mais tellement simple à obtenir que je ne vois pas pourquoi je m'en priverais. À l'origine, je tiens ce conseil d'Anubis qui m'a fait réaliser que dans l'un des cas, on a besoin d'une variable temporaire supplémentaire et non dans l'autre.
Grossièrement, c'est dû à ça (ces deux déclarations sont environ celles du C++, d'après les quelques souvenirs qu'il m'en reste. Avec ma chance, d'ailleurs, c'est sûrement l'inverse puisque je les remets au pif. Pas grave, l'objectif n'est pas de faire du C++ mais de faire saisir la différence entre la pré et la post-incrémentation) :
Post-incrémentation : i++
int operator++()
{
int sauvegarde = i;
this.i = this.i +1;
return sauvegarde;
}Pré-incrémentation : ++i
int operator++(int)
{
this.i = this.i +1;
return this.i;
} Si vous utilisez un
StringBufferavec plusieurs append statiques qui se suivent (merci Linux Magazine) :buffer.append("bonjour");
buffer.append("au revoir");Utilisez plutôt :
buffer.append("bonjour")
.append("au revoir");Vous éviterez ainsi d'empiler inutilement le retour de la méthode append sans vous en servir.


Commentaires
Euh... Le compilo ne peux pas tout seul remarquer dans le i++ du for qu'on n'a pas besoin de garder l'ancienne valeur ? et donc corriger le tir par lui même.
J'avais cru entendre que les trucs aussi simple il optimisait tout seul comme un grand...
En Java, c'est fort possible, mais ce n'est pas forcément le cas (je ne sais pas comment est codé la machine virtuelle). En C/C++, surtout si tu ne demandes pas au compilo d'optimiser, c'est beaucoup moins sûr.
Pour finir, comme je l'ai dit, puisque c'est si simple à penser, je ne préfère pas me reposer sur l'espoir que le compilateur fera un travail intelligent, je le fais moi-même pour être sûr :-).
En fait, pré ou post-incrémentation, cela ne fait pas de différence de performance. L'incrémentation d'un entier ne passe pas par un appel de fonction, il n'est donc pas nécessaire de créer une variable temporaire pour la valeur à retourner.
On copie la variable à utiliser, qu'elle ait été incrémentée avant ou après.
myFunction(i++);
Ca donne: -> Mettre la valeur courante de i sur la pile -> Incrémenter i -> Appeler myFunction(int)
Que ce soit en C, en C++ ou en Java.
Pour myFunction(++i);
Ca donne: -> Incrémenter i -> Mettre la valeur courante de i sur la pile -> Appeler myFunction(int)
Encore un commentaire :
Lorsqu'on utilise plusieurs append() avec une chaine statique, il vaut mieux faire la concaténation avec un +.
Ce code génère exactement ce qui est écrit :
buffer.append("bonjour") .append("au revoir");Nous avons deux appels de fonction (virtuelles au passage, ça veut dire qu'il faut les résoudre avant).
tandis que
buffer.append("bonjour"+"au revoir");ne génère qu'un seul appel avec "bonjourau revoir".Le compilateur reconnait qu'il s'agit de deux constantes dont il connait déjà la valeur, et dans ce cas les chaines "bonjour" et "au revoir" ne seronts pas créés dans l'objet, on aura directement "bonjourau revoir" généré à la compilation.
> myFunction(i++); Ca donne: -> Mettre la valeur courante de i sur la pile -> Incrémenter i -> Appeler myFunction(int)
Je ne suis pas d'accord. Tu tiens ici compte du fait que le compilateur « repère » le i++ et va sauvegarder la valeur de l'argument avant.
Un compilateur ne mélange pas les appels et l'opérateur i++ sera toujours exécuté AVANT l'exécution de la fonction. Étant exécuté AVANT, la valeur précédente devra être sauvegardé (pile, registres, malloc XD) mais en tout cas, cela fera toujours une affectation supplémentaire face à ++i.
C'est bien sûr le cas théorique sans aucune optimisation.
D'apres mes bouquins d'optimisation, le ++i sur entier est plus rapide sur une majorité de machines, mais pas sur la totalité.
Bien sûr, lors d'une surcharge d'opérateur la pré-incrementation est plus rapide que la post-incrementation quelle que soit la machine.
Pour ce qui est du dilemme sur l'assembleur généré avec un i++, suffit de tester... gcc -S
Bon je n'ai pas gcc sous la main en ce moment mais je promets de le faire.
Ok, bon mon gcc me fait l'optimisation expliqué par skc...
Voici le code C++:
int main() { int i = 0; int a = i++; int b = ++i; return 0; }Voici le code ASM généré (PowerPC) :
On peut donc dire que pour le cas des entiers, skc a raison en disant que les compilos savent faire ça bien. Maintenant, attention à ne pas généraliser aux autres objets plus complexes qui eux ne peuvent pas utiliser les registres pour une sauvegarde de leur état.
Bon maintenant 2e chose :D
Là je ne suis pas d'accord.
buffer.append() est une méthode qui ajoute des données à buffer... L'appeler 2 fois est donc normal pour ajouter 2 informations.
Maintenant buffer.append().append() ne fait tout simplement pas ce qu'on lui demande, en tout cas pas théoriquement...
buffer.append() ajoute des informations à buffer, puis la variable de retour temporaire se voit ajouter la nouvelle information. Heureusement que nous sommes en Java et que append retourne une référence vers l'objet venant d'être modifié, sinon l'ajout fait par le second append ne serait tout simplement pas pris en compte car appelé sur une variable temporaire.
Rahlala, ce genre de code, c'est jouer avec les limites de l'API, et ça n'a strictement aucun intéret si ce n'est obscurcir le code.
Pour ce qui est des variables retour, la méthodes étant appelée 2 fois dans les 2 cas, il y a aura bien 2 valeurs de retours empilées dans les 2 cas, donc aucune optimisation de ce côté.
PS: Batmat, il faudra penser à augmenter ce textarea s'il-te-plait
On ne peut même plus débattre sans suffoquer :D
Pas d'accord, tu généralises en disant que ça ne marcherait dans aucun autre cas alors que ça n'existe pas qu'en Java.
Ce principe de chaînage est aussi utilisé en C++ lorsque tu surcharge par exemple l'opérateur << afin de pouvoir écrire
cout << "bonjour" << "au revoir";
Non ?
Pardon, je ne voulais pas généraliser le fait que ce soit spécialement pour Java. Je voulais seulement montrer que ce n'était faisable que parce que la méthode le permettait. Il faut bien vérifier auparavant que la méthode (ou l'opérateur) renvoit une référence sur l'objet modifié. (Le bon vieux return *this; qui choque les débutants ^^)
Maintenant, enchainer plusieurs opérateurs comme tu le montres en C++ est une chose commune qui ne nuit pas — selon moi — à la lisibilité. Alors qu'un enchainement de méthodes fait souvent penser que l'on récupère un objet par le première sur lequel on applique la deuxième.
Il n'est — encore pour moi — pas naturel de me dire que la seconde méhode affectera le premier objet.
Tout ceci est bien sûr très subjectif et discutable et je serais incapable de t'expliquer pourquoi je trouve cela plus naturel avec des opérateurs qu'avec des méthodes.
Java ça pue le paté.
Toi, pour t'exprimer aussi brillamment, tu dois être le plus expérimenté de nous tous. Qu'attends tu pour nous expliquer de façon plus poussée tes vues et pensées sur le sujet. Peut-être connais-tu même d'autres langages ?
J'attends avec impatience ta parole, mon gourou de la programmation...
"Avec ma chance, d'ailleurs, c'est sûrement l'inverse puisque je les remets au pif. Pas grave, l'objectif n'est pas de faire du C++ mais de faire saisir la différence entre la pré et la post-incrémentation) :"
En effet, en C++, je viens de regarder, et c'est le contraire, c'est bien l'opérateur de post-incrémentation qui reçoit un argument fictif de type int.
Dommage ^^
Arthur, qui apporte sa contribution
Merci Arthur. C'est toujours agréable de clarifier les coins obscurs