La performance joue un rôle essentiel dans la réussite d'un projet logiciel. Les optimisations appliquées lors du développement back-end Java garantissent une utilisation efficace des ressources système et augmentent l'évolutivité de votre application.
Dans cet article, je partagerai avec vous quelques techniques d'optimisation que je considère importantes pour éviter les erreurs courantes.
Le choix d'une structure de données efficace peut améliorer considérablement les performances de votre application, en particulier lorsqu'il s'agit de grands ensembles de données ou d'opérations à temps critique. L'utilisation de la structure de données correcte minimise le temps d'accès, optimise l'utilisation de la mémoire et réduit le temps de traitement.
Par exemple, lorsque vous devez rechercher fréquemment dans une liste, l'utilisation d'un HashSet au lieu d'un ArrayList peut donner des résultats plus rapides :
// Inefficient - O(n) complexity for contains() check List<String> names = new ArrayList<>(); names.add("Alice"); names.add("Bob"); // Checking if "Alice" exists - time complexity O(n) if (names.contains("Alice")) { System.out.println("Found Alice"); } // Efficient - O(1) complexity for contains() check Set<String> namesSet = new HashSet<>(); namesSet.add("Alice"); namesSet.add("Bob"); // Checking if "Alice" exists - time complexity O(1) if (namesSet.contains("Alice")) { System.out.println("Found Alice"); }
Dans cet exemple, HashSet fournit une complexité temporelle moyenne de O(1) pour les opérations contain(), tandis qu'ArrayList nécessite O(n) car il doit parcourir la liste. Par conséquent, pour les recherches fréquentes, un HashSet est plus efficace qu’un ArrayList.
Au fait, si vous voulez savoir ce qu'est la complexité temporelle : la complexité temporelle fait référence à la façon dont le temps d'exécution d'un algorithme varie en fonction de la taille d'entrée. Cela nous aide à comprendre à quelle vitesse un algorithme s’exécute et montre généralement comment il se comporte dans le pire des cas. La complexité temporelle est communément désignée par Big O Notation.
Vous pouvez éviter une surcharge de traitement inutile en vérifiant les champs à utiliser dans la méthode qui ne doivent pas être nuls au début de la méthode. Il est plus efficace en termes de performances de les vérifier au début, au lieu de vérifier les vérifications nulles ou les conditions illégales dans les étapes ultérieures de la méthode.
public void processOrder(Order order) { if (Objects.isNull(order)) throw new IllegalArgumentException("Order cannot be null"); if (order.getItems().isEmpty()) throw new IllegalStateException("Order must contain items"); ... // Process starts here. processItems(order.getItems()); }
Comme le montre cet exemple, la méthode peut contenir d'autres processus avant d'accéder à la méthode processItems. Dans tous les cas, la liste Items dans l’objet Order est nécessaire au fonctionnement de la méthode processItems. Vous pouvez éviter un traitement inutile en vérifiant les conditions au début du processus.
La création d'objets inutiles dans les applications Java peut affecter négativement les performances en augmentant le temps de récupération de place. L'exemple le plus important est l'utilisation de String.
C'est parce que la classe String en Java est immuable. Cela signifie que chaque nouvelle modification de String crée un nouvel objet en mémoire. Cela peut entraîner une perte de performances importante, en particulier dans les boucles ou lorsque plusieurs concaténations sont effectuées.
La meilleure façon de résoudre ce problème est d'utiliser StringBuilder. StringBuilder peut modifier la chaîne sur laquelle il travaille et effectuer des opérations sur le même objet sans créer de nouvel objet à chaque fois, ce qui entraîne un résultat plus efficace.
Par exemple, dans le fragment de code suivant, un nouvel objet String est créé pour chaque opération de concaténation :
// Inefficient - O(n) complexity for contains() check List<String> names = new ArrayList<>(); names.add("Alice"); names.add("Bob"); // Checking if "Alice" exists - time complexity O(n) if (names.contains("Alice")) { System.out.println("Found Alice"); } // Efficient - O(1) complexity for contains() check Set<String> namesSet = new HashSet<>(); namesSet.add("Alice"); namesSet.add("Bob"); // Checking if "Alice" exists - time complexity O(1) if (namesSet.contains("Alice")) { System.out.println("Found Alice"); }
Dans la boucle ci-dessus, un nouvel objet String est créé avec chaque résultat = opération. Cela augmente à la fois la consommation de mémoire et le temps de traitement.
Nous pouvons éviter la création d'objets inutiles en faisant de même avec StringBuilder. StringBuilder améliore les performances en modifiant l'objet existant :
public void processOrder(Order order) { if (Objects.isNull(order)) throw new IllegalArgumentException("Order cannot be null"); if (order.getItems().isEmpty()) throw new IllegalStateException("Order must contain items"); ... // Process starts here. processItems(order.getItems()); }
Dans cet exemple, un seul objet est créé à l'aide de StringBuilder et des opérations sont effectuées sur cet objet tout au long de la boucle. En conséquence, la manipulation de la chaîne est terminée sans créer de nouveaux objets en mémoire.
La fonction flatMap incluse avec l'API Java Stream est un outil puissant pour optimiser les opérations sur les collections. Les boucles imbriquées peuvent entraîner une perte de performances et un code plus complexe. En utilisant cette méthode, vous pouvez rendre votre code plus lisible et gagner en performances.
map : Opère sur chaque élément et renvoie un autre élément en conséquence.
flatMap : Fonctionne sur chaque élément, convertit les résultats en une structure plate et fournit une structure de données plus simple.
Dans l'exemple suivant, les opérations sont effectuées avec des listes à l'aide d'une boucle imbriquée. À mesure que la liste s'allonge, l'opération deviendra de plus en plus inefficace.
String result = ""; for (int i = 0; i < 1000; i++) result += "number " + i;
En utilisant flatMap, nous pouvons nous débarrasser des boucles imbriquées et obtenir une structure plus propre et plus performante.
StringBuilder result = new StringBuilder(); for (int i = 0; i < 1000; i++) result.append("number ").append(i); String finalResult = result.toString();
Dans cet exemple, nous convertissons chaque liste en un flux plat avec flatMap puis traitons les éléments avec forEach. Cette méthode rend le code à la fois plus court et plus efficace en termes de performances.
Le renvoi direct des données extraites de la base de données en tant que classes d'entité peut entraîner un transfert de données inutile. Il s’agit d’une méthode très défectueuse tant en termes de sécurité que de performances. Au lieu de cela, l'utilisation de DTO pour renvoyer uniquement les données nécessaires améliore les performances de l'API et évite les transferts de données volumineux inutiles.
List<List<String>> listOfLists = new ArrayList<>(); for (List<String> list : listOfLists) { for (String item : list) { System.out.println(item); } }
Les performances de la base de données affectent directement la vitesse de l'application. L'optimisation des performances est particulièrement importante lors de la récupération de données entre des tables liées. À ce stade, vous pouvez éviter le chargement inutile de données en utilisant EntityGraph et Lazy Fetching. Dans le même temps, une indexation appropriée dans les requêtes de base de données améliore considérablement les performances des requêtes.
EntityGraph vous permet de contrôler les données associées dans les requêtes de base de données. Vous extrayez uniquement les données dont vous avez besoin, évitant ainsi les coûts liés à une récupération hâtive.
La récupération impatiente se produit lorsque les données de la table associée sont automatiquement accompagnées de la requête.
// Inefficient - O(n) complexity for contains() check List<String> names = new ArrayList<>(); names.add("Alice"); names.add("Bob"); // Checking if "Alice" exists - time complexity O(n) if (names.contains("Alice")) { System.out.println("Found Alice"); } // Efficient - O(1) complexity for contains() check Set<String> namesSet = new HashSet<>(); namesSet.add("Alice"); namesSet.add("Bob"); // Checking if "Alice" exists - time complexity O(1) if (namesSet.contains("Alice")) { System.out.println("Found Alice"); }
Dans cet exemple, les données liées à l'adresse et les informations utilisateur sont récupérées dans la même requête. Les requêtes supplémentaires inutiles sont évitées.
Contrairement à Eager Fetch, Lazy Fetch récupère les données des tables associées uniquement en cas de besoin.
public void processOrder(Order order) { if (Objects.isNull(order)) throw new IllegalArgumentException("Order cannot be null"); if (order.getItems().isEmpty()) throw new IllegalStateException("Order must contain items"); ... // Process starts here. processItems(order.getItems()); }
L'indexation est l'une des méthodes les plus efficaces pour améliorer les performances des requêtes de base de données. Une table dans une base de données est constituée de lignes et de colonnes, et lorsqu'une requête est effectuée, il est souvent nécessaire d'analyser toutes les lignes. L'indexation accélère ce processus, permettant à la base de données de rechercher plus rapidement sur un champ spécifique.
La mise en cache est le processus de stockage temporaire des données ou des résultats de calcul fréquemment consultés dans une zone de stockage rapide telle que la mémoire. La mise en cache vise à fournir ces informations plus rapidement lorsque les données ou le résultat du calcul sont à nouveau nécessaires. En particulier dans les requêtes de base de données et les transactions nécessitant des coûts de calcul élevés, l'utilisation du cache peut améliorer considérablement les performances.
L'annotation @Cacheable de Spring Boot rend l'utilisation du cache très simple.
String result = ""; for (int i = 0; i < 1000; i++) result += "number " + i;
Dans cet exemple, lorsque la méthode findUserById est appelée pour la première fois, les informations utilisateur sont récupérées de la base de données et stockées dans le cache. Lorsque les mêmes informations utilisateur sont à nouveau nécessaires, elles sont récupérées du cache sans accéder à la base de données.
Vous pouvez également utiliser une solution de mise en cache de premier ordre telle que Redis pour les besoins de votre projet.
Vous pouvez développer des applications plus rapides, plus efficaces et évolutives grâce à ces techniques d'optimisation, notamment dans vos projets back-end développés avec Java.
...
Merci d’avoir lu mon article ! Si vous avez des questions, des commentaires ou des réflexions que vous aimeriez partager, j'aimerais les entendre dans les commentaires.
Vous pouvez me suivre sur Dev.to pour plus d'informations sur ce sujet et mes autres articles. N'oubliez pas d'aimer mes publications pour les aider à toucher plus de personnes !
Pour me suivre sur LinkedIn : tamerardal
Ressources :
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!