Les Strings se déchaînent

Romain Guy (Gfx)

Les Strings sont un des objets les plus employés de Java. À tel point qu'il est même impossible
de créer une application Java sans en faire usage. Voyons comment les utiliser de manière optimale… ♪

- Titre : Les Strings se déchaînent

- Auteur : Romain Guy (Gfx)

- Parution : 11 septembre 2006

- Licence : Cette création est mise à disposition sous un contrat Creative Commons (Paternité - Partage des Conditions Initiales à l'Identique).

Sommaire

- Introduction

- I. 1 + 1 = 2

- II. +=, danger !

- III. Dilemme

- IV. Screenshots

Introduction

L'utilisation de l'objet String dans son code source est extrêmement aisée, mais hélas beaucoup trop souvent source de contre-performances. L'obtention d'un bytecode efficace ne sera possible que si vous apprenez à maîtriser les Strings. Afin d'y parvenir, nous allons tout d'abord voir comment ces objets sont gérés par les compilateurs.

I - 1 + 1 = 2

Les compilateurs Java permettent d'additionner directement les Strings entre elles, ce qui va à l'encontre même de leur nature. Les Strings sont en effet des objets et Java ne permet pas de surcharger les opérateurs. Que se passe-t-il donc ? En réalité, tout dépend du compilateur que vous utilisez. Imaginons le code suivant:

String _luke = new String("Luke");
String dialogue = "Bonjour " + _luke;

La dernière ligne est traduite automatiquement par le compilateur de Sun (javac) ou d'IBM (jikes) en:

dialogue = new StringBuffer("Bonjour ").append(_luke).toString();

Le compilateur crée donc une instance de la classe StringBuffer avec le premier membre, puis appelle la méthode append() pour y ajouter le second membre. Enfin, la méthode toString() convertit l'objet en instance de String. Ainsi, additionner plusieurs chaînes donnera l'équivalent bytecode de :

new StringBuffer("Bonjour ").append(_luke).append(_chewie).append(_solo);

Il faut ici noter que l'addition de constantes sera optimisée par le compilateur pour ne créer qu'une instance de String.

String _str = "Login: " + "numéro " + 67;

sera interprété de cette manière :

String _str = "Login numéro 67";

Si vous additionnez un objet différent de String, le compilateur appellera toujours la méthode append() de StringBuffer qui appellera elle même la méthode toString() de l'objet. L'addition d'un type primitif provoquera également l'appel de la méthode append() spécifique. Imaginons maintenant le cas où le premier membre n'est pas une constante String. Le compilateur de Sun traduira ce code :

String _str;
String _luke = "Luke";
_str = "a" + _luke;
_str = 'a' + _luke;

de la manière suivante :

new StringBuffer("a").append(_luke);
new StringBuffer().append('a').append(_luke);

Par contre, jikes créera les instructions :

new StringBuffer("a").append(_luke);
new StringBuffer("a").append(_luke);

Jikes possède ici un petit désavantage dans la mesure où il contraint la JVM a créer une nouvelle instance de String même dans l'hypothèse où nous n'ajoutons qu'un seul et unique caractère. Vous pouvez vous même vérifier comment votre compilateur traduit votre code (ces transpositions de String à StringBuffer ayant la fâcheuse tendance à changer au fil des versions des compilateurs) grâce à l'outil javap. Javap est livré avec le JDK et son utilisation est des plus simples :

javap -c ClasseAAnalyser

Javap accepte également l'option classpath qui s'utilise de la même manière que pour javac ou java. Faites attention: par défaut, javap ne décompile pas les méthodes private. Une analyse minutieuse du listing produit par javap vous servira ainsi à comprendre comment optimiser votre code source.

II - +=, danger !

Comme vous avez pu le constater, les compilateurs font un usage immodéré de la classe StringBuffer. Dans un cadre d'utilisation simple, par exemple lorsqu'il s'agit d'afficher un unique message, l'utilisation de la classe String se suffit à elle même. Mais il est parfois préférable de manipuler directement une instance de StringBuffer. Ceci est particulièrement vrai lorsque la chaîne doit être construite dans une boucle:

public String createStringFromArray(Object[] array)
{
String _ret = new String();
for (int i = 0; i < array.length; i++)
_ret += array[i];

return _ret;
}

Cette méthode est un très mauvais exemple de l'utilisation de l'opérateur += sur les String. Sachant que l'expression a += b est automatiquement traduite par le compilateur en a = a + b, essayez de trouver qui se passera si nous additionnons une String et un caractère avec +=. Vous l'avez deviné :

_ret = new StringBuffer(_ret).append(array[i]).toString();

Vous constaterez qu'à chaque passage de la boucle une nouvelle instance de StringBuffer est créée. Or la création d'instances est l'une des opérations les plus coûteuses en termes de performances. La solution serait de pouvoir récupérer l'instance temporaire de StringBuffer créée par le compilateur. Hélas cela n'est pas possible. La solution consiste donc à utiliser uniquement StringBuffer.

public void createStringFromArray(Object[] array)
{
StringBuffer _buf = new StringBuffer();
for (int i = 0; i < array.length; i++)
_buf.append(array[i]);

return _buf.toString();
}

Cette fois-ci, seule une instance de StringBuffer se voit créée. Le gain offert lors de l'exécution est non négligeable. Pour vous donner une idée, avec un tableau de 10 000 objets, la première version de la méthode s'exécute en 14 secondes tandis que la seconde version ne requiert que 170 millisecondes !! Bien qu'impressionnant, ce résultat peut encore être optimisé. En effet, la classe StringBuffer possède un constructeur permettant de définir la taille du tampon contenant la chaîne. La valeur par défaut est de 16. Lors d'un appel à append(), il se peut que le tampon soit plein. En ce cas, un nouveau tampon double est créé et l'ancien est copié dans le nouveau. Lors de la copie, l'instance de StringBuffer contient donc deux tampons : l'ancien et le nouveau de taille double. Avec un ancien tampon possédant déjà une taille conséquente, les exigences en termes de mémoire seront très importantes ! Spécifier la taille du tampon permet d'éviter la copie et donc de perdre du temps et de la mémoire. Dans notre exemple, remplacer la ligne créant le StringBuffer par:

StringBuffer _buf = new StringBuffer(array.length);

servira à définir une taille de tampon qui ne sera jamais dépassée. Le tampon ne sera jamais copié et l'exécution de la méthode se fera alors en 100 millisecondes seulement.
Et même si vous pensez rien ne vaut que la classe String pour manipuler du texte, ne vous insurgez pas contre le choix du StringBuffer. En prenant soin de lire la documentation des classes standards du JDK, vous constaterez que StringBuffer possède quelques-unes des méthodes les plus utilisées avec les String. En vrac: charAt(), substring(), replace(), length()… Et si par malheur vous devez faire appel à une méthode spécifique à String, la méthode toString() et le constructeur StringBuffer(String str) vous rendront de fiers services. Un autre intérêt du StringBuffer et sa capacité à se voir modifier beaucoup plus aisément que son homologue String. Ainsi, changer un caractère au milieu d'une chaîne requiert de savantes manipulations à l'aide de substring() dans le cas d'une String. À l'inverse, apporter la même modification à la chaîne par le biais d'un StringBuffer ne demande qu'un appel à la méthode setCharAt(). Je ne puis que vous conseiller de lire avec attention la documentation de Sun pour découvrir la puissance de StringBuffer.

III - Dilemme

Maintenant que vous voilà convaincu de l'intérêt d'optimiser l'utilisation des chaînes de caractères, et de faire usage de la classe StringBuffer, il vous reste à faire un choix. Les compilateurs de Sun et d'IBM, bien que cela soit moins flagrant que par le passé, proposent tous les deux une conversion différente des additions de chaînes lors de la compilation. Il vous faut évidemment écrire votre code afin de produire les exécutables les plus efficaces en fonction du compilateur que vous utilisez. Si, comme bon nombre de développeurs Java, vous utilisez Jikes, pour sa vélocité, lors du développement et javac avant de distribuer les binaires de votre application, n'hésitez pas une seconde: optimisez votre code pour javac. N'ayez aucun regret, car ce dernier offre en règle générale, et pour les Strings en particulier, de meilleures performances que Jikes.

IV - Screenshots

Combien de Strings utilisent ce logiciel ?

Plongez vous dans la documentation

L'addition de Strings selon les compilateurs

javap permet de comprendre comment javac…

..et jikes traduisent votre code