W3docs

Conseils de performance Java

Conseils pratiques pour optimiser les performances Java : mesurer d'abord, éviter l'optimisation prématurée, micro-optimisations courantes.

Java est suffisamment rapide pour presque tout, mais du code lent existe quand même — généralement parce qu'il fait trop de travail, alloue trop de mémoire ou utilise la mauvaise structure de données. Le conseil de performance le plus important n'est pas une astuce : mesurez avant de changer quoi que ce soit. Ce chapitre explique comment mesurer honnêtement, présente les quelques optimisations les plus rentables (construction de chaînes, choix de structure de données, éviter les allocations inutiles) et les pièges qui rendent le code naïvement « rapide » en réalité lent.

Mesurer d'abord, optimiser ensuite

Deviner les performances, c'est passer des heures à accélérer du code qui n'était pas lent. Profilez une charge de travail réelle, trouvez le chemin critique (la petite fraction du code où la plupart du temps est réellement dépensé), puis optimisez-le seulement. Pour des expériences rapides, System.nanoTime() donne un delta d'horloge murale ; pour des benchmarks sérieux, utilisez un outil comme JMH (Java Microbenchmark Harness), qui chauffe le JIT et tient compte du bruit de mesure.

long start = System.nanoTime();
doWork();
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("Took " + elapsedMs + " ms");

Deux règles accompagnent cela. Premièrement, évitez l'optimisation prématurée — un code clair et suffisamment rapide vaut mieux qu'un code astucieux difficile à lire. Deuxièmement, le compilateur JIT de la JVM optimise le code chaud à l'exécution, donc une méthode n'atteint sa pleine vitesse qu'après avoir été exécutée de nombreuses fois ; un seul appel chronométré ne vous apprend pas grand-chose.

Construire des chaînes avec StringBuilder

Parce que les chaînes sont immuables, s += x dans une boucle crée une toute nouvelle chaîne et copie chaque caractère précédent à chaque itération — c'est un travail en O(n²). StringBuilder conserve un tampon extensible et ajoute sur place, transformant le même travail en O(n).

// Slow: a new String allocated every iteration
String csv = "";
for (String field : fields) {
    csv += field + ",";
}

// Fast: one buffer, appended in place
StringBuilder sb = new StringBuilder();
for (String field : fields) {
    sb.append(field).append(',');
}
String csv2 = sb.toString();

Un simple a + b + c hors d'une boucle est acceptable — le compilateur le transforme déjà en un seul StringBuilder (voir la concaténation de chaînes pour ce que fait le compilateur). Le problème est la concaténation à l'intérieur d'une boucle, où chaque passage ajoute une copie complète de plus.

Choisir la bonne structure de données

Les plus grands gains viennent généralement des choix algorithmiques, pas des micro-ajustements. Rechercher une valeur dans un ArrayList parcourt chaque élément (O(n)) ; un HashMap ou HashSet le fait en temps approximativement constant (O(1)). Choisissez la collection qui correspond à la façon dont vous accédez réellement aux données.

BesoinUtiliserCoût de recherche
Accès par index, ajout en finArrayListO(1) par index, O(n) par valeur
Recherche clé/valeurHashMapO(1) en moyenne
Test d'appartenance, sans doublonsHashSetO(1) en moyenne
Clés triéesTreeMapO(log n)
Insertions/suppressions fréquentes aux extrémitésArrayDequeO(1) aux extrémités

Si vous connaissez la taille finale, passez-la au constructeur : new ArrayList<>(10_000) ou new HashMap<>(capacity). Cela évite les réallocations et copies répétées qui surviennent lorsqu'une collection grandit.

Éviter la création d'objets inutiles

Chaque objet alloué doit ensuite être collecté, et le ramasse-miettes n'est pas gratuit. Réutilisez les valeurs immuables, préférez les primitives à leurs équivalents encadrés dans les boucles serrées, et ne créez pas d'objets que vous jetez immédiatement.

// Autoboxing: every += boxes a new Integer
Long total = 0L;
for (int i = 0; i < n; i++) total += i;   // slow, allocates boxes

// Primitive: no allocation at all
long sum = 0L;
for (int i = 0; i < n; i++) sum += i;     // fast

D'autres gains faciles : mettez en cache les objets Pattern compilés au lieu d'appeler String.matches() dans une boucle, réutilisez un DateTimeFormatter (il est thread-safe et immuable), et préférez le for amélioré aux streams dans les boucles internes les plus chaudes où l'allocation compte.

java— editable, runs on the server

Ce que l'on retient de l'exécution :

  • Same result? true prouve que StringBuilder produit exactement la même chaîne que +=, donc le remplacer ne change que la vitesse, jamais le résultat.
  • Le ratio « x slower » affiché montre que la concaténation en boucle coûte plusieurs fois plus cher que l'ajout dans un tampon, car chaque += copie toute la chaîne construite jusqu'alors.
  • Both lists size 100000: true confirme qu'un ArrayList pré-dimensionné finit identique à un qui a grandi — l'indication du constructeur affecte l'allocation, pas le contenu.
  • Pre-sized faster? true montre qu'indiquer sa capacité à ArrayList dès le départ évite les étapes répétées de redimensionnement et de copie.
  • Map lookups found: 50000 in ... ms démontre que 50 000 recherches dans un HashMap se terminent en environ une milliseconde, bénéfice du choix d'un accès O(1) plutôt qu'un parcours O(n) de liste.

Pièges courants

Quelques erreurs transforment du code « manifestement plus rapide » en son contraire :

  • Se fier à une seule mesure chronométrée. Le JIT n'est pas encore chaud, et le système d'exploitation peut avoir planifié autre chose au milieu de la mesure. Répétez le travail des milliers de fois, ou utilisez JMH, avant de croire un chiffre.
  • Micro-optimiser du code froid. Une méthode qui s'exécute une fois au démarrage ne tire rien d'une boucle plus serrée. Ne dépensez de l'effort que sur le chemin critique que le profileur indique.
  • Construire des chaînes avec + dans une boucle. Le ralentissement évitable le plus courant — utilisez StringBuilder dès que vous concaténez dans une boucle.
  • L'autoboxing caché. Un List<Integer>, Map<Integer, Integer> ou un accumulateur Long encadre chaque valeur. Dans une boucle numérique serrée, préférez les primitives et les tableaux de primitives.
  • Optimiser avant que ça fonctionne. D'abord correct, ensuite rapide. Un code clair que vous pouvez profiler vaut mieux qu'un code astucieux sur lequel vous ne pouvez pas raisonner.

Récapitulatif

  • Mesurez avant de changer quoi que ce soit — profilez une charge de travail réelle et optimisez uniquement le chemin critique.
  • Construisez des chaînes avec StringBuilder, pas +=, dans les boucles.
  • Adaptez la collection au schéma d'accès : HashMap/HashSet pour les recherches, ArrayList pour l'accès indexé ; pré-dimensionnez quand le nombre est connu.
  • Évitez les allocations inutiles : préférez les primitives, réutilisez les objets immuables, et rappelez-vous que chaque objet ajoute du travail au ramasse-miettes.

Pratique

Pratique
Pourquoi utiliser '+=' pour construire une String dans une boucle est-il lent par rapport à StringBuilder ?
Pourquoi utiliser '+=' pour construire une String dans une boucle est-il lent par rapport à StringBuilder ?
Was this page helpful?