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.
| Besoin | Utiliser | Coût de recherche |
|---|---|---|
| Accès par index, ajout en fin | ArrayList | O(1) par index, O(n) par valeur |
| Recherche clé/valeur | HashMap | O(1) en moyenne |
| Test d'appartenance, sans doublons | HashSet | O(1) en moyenne |
| Clés triées | TreeMap | O(log n) |
| Insertions/suppressions fréquentes aux extrémités | ArrayDeque | O(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; // fastD'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.
Ce que l'on retient de l'exécution :
Same result? trueprouve queStringBuilderproduit 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: trueconfirme qu'unArrayListpré-dimensionné finit identique à un qui a grandi — l'indication du constructeur affecte l'allocation, pas le contenu.Pre-sized faster? truemontre qu'indiquer sa capacité àArrayListdès le départ évite les étapes répétées de redimensionnement et de copie.Map lookups found: 50000 in ... msdémontre que 50 000 recherches dans unHashMapse 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 — utilisezStringBuilderdès que vous concaténez dans une boucle. - L'autoboxing caché. Un
List<Integer>,Map<Integer, Integer>ou un accumulateurLongencadre 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/HashSetpour les recherches,ArrayListpour 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.