
Je me souviens encore vivamment du jour où j’ai élaboré avec assurance un morceau de code en utilisant les Java Streams, convaincu d’avoir maîtrisé toutes leurs complexités. Le code apparaissait impeccable, épuré et sophistiqué — jusqu’à ce qu’un bug inattendu bouleverse notre système de production.
Quand l’élégance du code devient trompeuse
Dans la communauté des développeurs Java, j’appréciais, comme beaucoup, la fluidité que les Streams apportaient au codage. Un jour, confronté à la tâche de gérer une liste d’utilisateurs, mon objectif était de filtrer ceux qui étaient inactifs, de les transformer magiquement en objets de transfert de données (DTO) et de les compiler ensuite en une liste. C’était le scénario idéal pour exploiter les Streams !
List<UserDTO> activeUsers = users.stream()
.filter(User::isActive)
.map(UserDTO::new)
.collect(Collectors.toList());
Ça a l’air impeccable, non ? Cette solution fonctionnait parfaitement lors des phases de test. Pourtant, en production, il s’est avéré que nous rencontrions des problèmes de performances significatifs. Une séance de débogage exhaustive a permis de dévoiler le problème.
Le piège qui rôdait en embuscade
Où était le problème ? Le coupable se cachait dans le constructeur UserDTO
qui dissimulait sournoisement un appel de base de données !
public UserDTO(User user) {
this.id = user.getId();
this.name = user.getName();
this.orders = orderService.getOrdersByUserId(user.getId()); // Une erreur flagrante !
}
En utilisant .map(UserDTO::new)
, j’avais sans m’en rendre compte initié N appels de base de données — un par utilisateur — au lieu d’une opération par lot unique et efficace. Par conséquent, en gérant des listes avec des milliers d’utilisateurs, notre base de données a subi une surcharge, ralentissant les opérations à presque un arrêt complet.
Mettre en œuvre la solution
La solution s’est avérée être à la fois simple et cruciale : initier une requête par lot pour récupérer toutes les commandes avant de mapper les utilisateurs.
Map<Long, List<Order>> ordersMap = orderService.getOrdersForUserIds(
users.stream().map(User::getId).collect(Collectors.toList())
);
List<UserDTO> activeUsers = users.stream()
.filter(User::isActive)
.map(user -> new UserDTO(user, ordersMap.get(user.getId())))
.collect(Collectors.toList());
Au lieu d’un déluge de milliers de requêtes, ce changement a concentré nos opérations en une seule requête par lot, allégeant considérablement la charge de notre base de données.
Enseignements et leçons tirées
- Même si la puissance des Streams est indéniable, ils ne sont pas une panacée pour l’optimisation des performances.
- Méfiez-vous des effets secondaires cachés dans
map()
oufilter()
. - Évaluez rigoureusement les implications de vos opérations de transformation sur la base de données.
Cette épreuve m’a enseigné une leçon précieuse : le relâchement dans la compréhension des Java Streams a conduit à cet épisode humble mais éclairant. Et vous, avez-vous déjà rencontré un piège aussi insaisissable dans les Streams ?