1. Problème : pourquoi les Stream ordinaires ne sont pas toujours efficaces
Lorsque vous travaillez avec des collections de nombres en Java et que vous utilisez des flux classiques (Stream<Integer>, Stream<Double>), il se produit en coulisse du « boxing » et de l’« unboxing » des valeurs primitives en objets d’enveloppe (Integer, Double, etc.). C’est pratique, mais pas toujours efficace :
- Boxing — conversion d’un primitif (int) en objet (Integer).
- Unboxing — opération inverse : de l’objet vers le primitif.
Problème :
Le boxing/unboxing ajoute des opérations et consomme de la mémoire. Dans les zones chaudes (appelées très fréquemment) d’un programme, cela peut entraîner une baisse notable des performances, surtout si vous traitez de grands tableaux de nombres.
Exemple :
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().map(x -> x * 2).reduce(0, Integer::sum);
Ici, chaque nombre est un objet Integer, et non un primitif int.
2. Flux primitifs : IntStream, LongStream, DoubleStream
Pour éviter le boxing/unboxing superflu, Java propose des flux primitifs :
- IntStream — pour int
- LongStream — pour long
- DoubleStream — pour double
Ils ne manipulent que des primitifs et n’allouent pas d’objets d’enveloppe inutiles.
Comment créer un flux primitif ?
À partir d’un tableau :
int[] arr = {1, 2, 3, 4, 5};
IntStream s = Arrays.stream(arr);
Avec range/rangeClosed :
IntStream.range(0, 10) // 0..9
IntStream.rangeClosed(1, 5) // 1..5 inclus
Génération de nombres aléatoires :
new Random().ints(5, 0, 100) // 5 int aléatoires de 0 à 99
Exemple : somme des éléments d’un tableau
int[] arr = {1, 2, 3, 4, 5};
int sum = Arrays.stream(arr).sum(); // 15
3. Conversion entre Stream<T> et flux primitifs
Parfois vous avez un flux classique, parfois un flux primitif. Pour convertir, on utilise des méthodes dédiées :
- mapToInt, mapToLong, mapToDouble — convertissent un flux classique en flux primitif.
- boxed() — convertit un flux primitif en flux d’objets.
Exemple :
List<String> words = List.of("Java", "Stream", "API");
IntStream lengths = words.stream().mapToInt(String::length);
lengths.forEach(System.out::println); // 4 6 3
Dans l’autre sens :
IntStream ints = IntStream.range(1, 5);
Stream<Integer> boxed = ints.boxed();
Attention :
boxed() est l’opération inverse : elle recrée des objets d’enveloppe (Integer, Double, etc.). Si vous visez la performance maximale, évitez-la dans les zones chaudes.
4. Somme et statistiques : sum, average, min, max, summaryStatistics
Les flux primitifs offrent des méthodes pratiques d’agrégation :
- sum() — somme de tous les éléments
- average() — moyenne (retourne un OptionalDouble)
- min(), max() — minimum et maximum (OptionalInt, OptionalLong, OptionalDouble)
- summaryStatistics() — retourne un objet contenant la statistique complète (somme, moyenne, minimum, maximum, comptage)
Exemple :
int[] arr = {1, 2, 3, 4, 5};
IntSummaryStatistics stats = Arrays.stream(arr).summaryStatistics();
System.out.println(stats.getSum()); // 15
System.out.println(stats.getAverage()); // 3.0
System.out.println(stats.getMin()); // 1
System.out.println(stats.getMax()); // 5
System.out.println(stats.getCount()); // 5
Comparaison avec Collectors :
Pour les flux classiques, on peut utiliser Collectors.summarizingInt, mais c’est souvent moins efficace que les méthodes des flux primitifs :
List<Integer> nums = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = nums.stream().collect(Collectors.summarizingInt(x -> x));
5. Éviter l’autoboxing : quand c’est important et comment le mesurer
Où est-ce critique ?
- Dans les boucles et flux qui traitent de gros volumes de nombres (par exemple, traitement de tableaux, statistiques, calculs, parsing de données).
- Dans les « zones chaudes » — du code appelé fréquemment et impactant la performance.
Pourquoi est-ce important ?
- À chaque boxing, un nouvel objet d’enveloppe est créé (Integer, Double, etc.).
- Cela augmente la charge sur le garbage collector (GC).
- Dans des microbenchmarks, la différence peut être très importante !
Comment mesurer ?
Pour une comparaison précise, on utilise le framework JMH (Java Microbenchmark Harness). Il permet de comparer honnêtement les performances d’un code avec et sans boxing.
Exemple (pseudo-code) :
@Benchmark
public int sumIntStream() {
return IntStream.range(0, 1_000_000).sum();
}
@Benchmark
public int sumStreamInteger() {
return Stream.iterate(0, n -> n + 1).limit(1_000_000).reduce(0, Integer::sum);
}
La deuxième variante sera plus lente en raison de la création constante d’objets Integer.
Conclusion :
Si la performance compte pour vous, utilisez les flux primitifs !
6. Quand les flux primitifs aident vraiment, et quand il ne faut pas compliquer
Utilisez des flux primitifs si :
- Vous travaillez avec de grands tableaux/collections de nombres.
- Vous devez calculer rapidement somme, moyenne, minimum, maximum.
- Vous écrivez du code où chaque milliseconde compte (par exemple, traitement de données en temps réel).
Vous pouvez ne pas vous en préoccuper si :
- La collection est petite (quelques dizaines d’éléments).
- Le code devient trop complexe à cause des conversions entre types.
- La performance n’est pas critique (par exemple, traitement de saisie utilisateur).
Exemple :
List<Integer> smallList = List.of(1, 2, 3);
int sum = smallList.stream().mapToInt(x -> x).sum(); // C’est possible, mais un reduce classique convient aussi
Conseil : ne transformez pas votre code en « jungle » pour une optimisation microscopique. Utilisez les flux primitifs là où c’est réellement justifié.
7. OptionalInt, OptionalDouble : extraction sûre des résultats
Les méthodes min(), max(), average() des flux primitifs ne retournent pas simplement un nombre, mais une « enveloppe » — OptionalInt, OptionalDouble, etc. Cela permet de gérer en toute sécurité les flux vides (par exemple, si le tableau est vide).
Exemple :
int[] arr = {};
OptionalInt min = Arrays.stream(arr).min();
if (min.isPresent()) {
System.out.println("Minimum : " + min.getAsInt());
} else {
System.out.println("Le tableau est vide !");
}
Comparaison avec Optional classique :
- OptionalInt — pour int
- OptionalDouble — pour double
- OptionalLong — pour long
Pourquoi ne pas simplement retourner 0 ?
Parce que 0 peut être une valeur valide, tandis qu’un flux vide est une situation distincte.
Exemple avec average :
double[] arr = {};
OptionalDouble avg = Arrays.stream(arr).average();
double result = avg.orElse(Double.NaN); // si vide — renverra NaN
8. Pratique : exemples d’utilisation des flux primitifs
Exemple 1 : Somme des carrés des nombres de 1 à 1000
int sum = IntStream.rangeClosed(1, 1000)
.map(x -> x * x)
.sum();
System.out.println(sum);
Exemple 2 : Filtrage et comptage des nombres pairs
int[] arr = {1, 2, 3, 4, 5, 6};
long count = Arrays.stream(arr)
.filter(x -> x % 2 == 0)
.count();
System.out.println("Nombres pairs : " + count);
Exemple 3 : Comparaison avec un Stream<Integer> classique
List<Integer> list = IntStream.range(0, 1_000_000)
.boxed()
.collect(Collectors.toList());
long t1 = System.currentTimeMillis();
int sum1 = list.stream().mapToInt(x -> x).sum();
long t2 = System.currentTimeMillis();
System.out.println("Stream<Integer>: " + (t2 - t1) + " ms");
t1 = System.currentTimeMillis();
int sum2 = IntStream.range(0, 1_000_000).sum();
t2 = System.currentTimeMillis();
System.out.println("IntStream: " + (t2 - t1) + " ms");
Sur de gros volumes, la différence sera notable.
9. Erreurs typiques lors de l’utilisation des flux primitifs
Erreur n°1 : oublier le boxing lors des conversions de types.
Si vous utilisez boxed(), souvenez-vous que cela recrée des objets d’enveloppe. Évitez-le sans nécessité.
Erreur n°2 : utiliser des flux primitifs pour des objets.
IntStream ne fonctionne qu’avec des int, pas avec des objets. Si vous devez manipuler des objets, utilisez un Stream<T> classique.
Erreur n°3 : ignorer OptionalInt/OptionalDouble.
Si vous appelez min(), max(), average(), vérifiez toujours que le résultat est présent (isPresent()), sinon vous obtiendrez une exception.
Erreur n°4 : transformations trop complexes entre Stream<T> et IntStream.
Si le code devient illisible à cause des mapToInt() → boxed() → mapToDouble() répétés, il est peut-être temps de simplifier la logique.
Erreur n°5 : attendre une accélération « magique » sur de petites collections.
Pour de petites listes, la différence entre Stream et IntStream est minime. Inutile de compliquer le code pour une économie microscopique.
GO TO FULL VERSION