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.