Aller au contenu principal

Bonnes pratiques et pièges

Streams vs Boucles

Les Streams et les boucles for produisent les mêmes résultats, mais avec des compromis différents.

CritèreStreamBoucle for
LisibilitéDéclaratif, concisVerbeux, mais familier
ParallélisationFacile (parallelStream())Manuelle
PerformanceOverhead léger (boxing/unboxing)Plus rapide sur petites collections
DebugPlus difficileFacile (breakpoints)
// Stream : lisible, chaînable
list.stream()
.filter(x -> x > 10)
.map(x -> x * 2)
.collect(Collectors.toList());

// Boucle : plus rapide pour petites listes
List<Integer> result = new ArrayList<>();
for (int x : list) {
if (x > 10) {
result.add(x * 2);
}
}

Règle pratique : utilisez les Streams pour la clarté et la parallélisation. Revenez aux boucles si la performance est critique sur de petites collections.


Immutabilité

Un objet immutable ne peut pas être modifié après sa création (String, record, champs final).

Avantages

  • Thread-safe sans synchronisation
  • Pas d'effets de bord
  • Sûr comme clé de HashMap

Inconvénients

  • Création de nouveaux objets à chaque modification (mémoire)
  • Moins flexible
// MAUVAIS : crée 10 000 objets String intermédiaires
String result = "";
for (int i = 0; i < 10000; i++) {
result += i;
}

// BON : modification in-place avec StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString();

Règle pratique : préférez l'immutabilité par défaut. Utilisez des objets mutables (StringBuilder, collections) uniquement quand la performance l'exige.


Évaluation paresseuse (Lazy) vs immédiate (Eager)

Les opérations intermédiaires d'un Stream sont paresseuses : elles ne s'exécutent pas tant qu'une opération terminale n'est pas appelée.

List<Integer> nombres = List.of(1, 2, 3, 4, 5);

// Rien ne s'exécute ici — le stream est juste configuré
Stream<Integer> stream = nombres.stream()
.filter(n -> {
System.out.println("filter: " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("map: " + n);
return n * 10;
});

// C'est seulement ici que tout s'exécute
List<Integer> result = stream.collect(Collectors.toList());

Avantages du lazy

  • Pas de travail inutile : si on ajoute .limit(1), Java s'arrête dès le premier élément trouvé.
  • Mémoire : les éléments sont traités un par un, sans créer de collections intermédiaires.

Piège du lazy

// Ce stream ne fait RIEN — il n'y a pas d'opération terminale
nombres.stream().filter(n -> n > 2).map(n -> n * 2);

Règle pratique : un Stream sans opération terminale ne produit aucun résultat. Toujours terminer avec collect(), forEach(), count(), etc.


Streams vs Collections

Un Stream n'est pas une structure de données — c'est un pipeline de traitement. Une Collection stocke des données.

CritèreStreamCollection
StockageNon (traitement à la volée)Oui
RéutilisableNon (consommé une fois)Oui
TaillePeut être infinieFinie
Accès aléatoireNonOui (get(i))
List<Integer> liste = List.of(1, 2, 3, 4, 5); // Collection — réutilisable

Stream<Integer> stream = liste.stream(); // Stream — usage unique
stream.forEach(System.out::println);
stream.forEach(System.out::println); // ERREUR : stream déjà consommé

Règle pratique : utilisez une Collection pour stocker et réutiliser des données, un Stream pour les transformer et les traiter.


Boxing / Unboxing et Streams primitifs

Avec Stream<Integer>, chaque int est emballé dans un objet Integer (boxing), ce qui a un coût en mémoire et en performance.

// Boxing : chaque int devient un objet Integer
Stream<Integer> stream = List.of(1, 2, 3).stream();
int somme = stream.mapToInt(Integer::intValue).sum();

// Pas de boxing : IntStream travaille directement avec des int
int somme = IntStream.of(1, 2, 3).sum();

Quand utiliser les streams primitifs ?

  • IntStream, LongStream, DoubleStream → opérations numériques sur de grandes quantités de données
  • Méthodes spécialisées disponibles : sum(), average(), min(), max(), range()
// Moyenne des carrés de 1 à 100
double moyenne = IntStream.rangeClosed(1, 100)
.map(n -> n * n)
.average()
.orElse(0);

Règle pratique : dès que tu travailles avec des int, long ou double en grande quantité, préfère les streams primitifs pour éviter le surcoût du boxing.


Debugging des Streams

Les streams sont plus difficiles à déboguer que les boucles car le pipeline est opaque.

// Difficile à déboguer : où est le problème ?
List<String> result = personnes.stream()
.filter(p -> p.getAge() > 18)
.map(Personne::getNom)
.sorted()
.collect(Collectors.toList());

// Avec peek() : inspecter les éléments à chaque étape
List<String> result = personnes.stream()
.filter(p -> p.getAge() > 18)
.peek(p -> System.out.println("après filter: " + p))
.map(Personne::getNom)
.peek(n -> System.out.println("après map: " + n))
.sorted()
.collect(Collectors.toList());

peek() est une opération intermédiaire qui permet d'observer les éléments sans les modifier — utile uniquement pour le débogage.

Règle pratique : utilisez peek() pour déboguer un pipeline, mais retirez-le en production.