Les Strings se déchaînent

Les Strings sont un des objets les plus employés de Java. A 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...

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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.

1. 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:

 
Sélectionnez

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:

 
Sélectionnez

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:

 
Sélectionnez

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.

 
Sélectionnez

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

sera interprété de cette manière:

 
Sélectionnez

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:

 
Sélectionnez

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

de la manière suivante:

 
Sélectionnez

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

Par contre, jikes créera les instructions:

 
Sélectionnez

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 :

 
Sélectionnez

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.

2. +=, 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:

 
Sélectionnez

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é:

 
Sélectionnez

_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'instance 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.

 
Sélectionnez

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:

 
Sélectionnez

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 milli-econdes 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. A 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.

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

4. Screenshots

Combien de Strings utilisent ce logiciel ?
Combien de Strings utilisent ce logiciel ?
Plongez vous dans la documentation
Plongez vous dans la documentation
L'addition de Strings selon les compilateurs
L'addition de Strings selon les compilateurs
javap permet de comprendre comment javac...
javap permet de comprendre comment javac...
..et jikes traduisent votre code
..et jikes traduisent votre code
Articles et tutoriels Java
L'essentiel de Java en une heure
L'API java.nio du JDK 1.4
Inversion de contrôle en Java
L'introspection
Le Java Community Process
Conception de tests unitaires avec JUnit
Les Strings se déchaînent
Présentation de SWT
La programmation réseau en Java avec les sockets
Du bon usage de l'héritage et de la composition
Les références et la gestion de la mémoire en Java
Constructeurs et méthodes exportées en Java
Les membres statiques, finaux et non immuables en Java
Les classes et objets immuables en Java
Comprendre et optimiser le Garbage Collector
Les principes de la programmation d'une interface graphique
Les opérateurs binaires en Java
Prenez le contrôle du bureau avec JDIC
Les Java Data Objects (JDO version 1.0.1)
La persistance des données avec Hibernate 2.1.8
Journalisation avec l'API Log4j
Java 5.0 et les types paramétrés
Les annotations de Java 5
Java 1.5 et les types paramétrés
Créer un moteur de recherche avec Lucene
Articles et tutoriels Swing
Threads et performance avec Swing
Rechercher avec style en utilisant Swing
Splash Screen avec Swing et Java3D
Drag & Drop avec style en utilisant Swing
Attendre avec style en utilisant Swing
Mixer Java3D et Swing
Articles et tutoriels Java Web
Redécouvrez le web avec Wicket
Cette création est mise à disposition sous un contrat Creative Commons (Paternité - Partage des Conditions Initiales à l'Identique).