1. Problema: por qué los Stream normales no siempre son eficientes
Cuando trabajas con colecciones de números en Java y usas streams normales (Stream<Integer>, Stream<Double>), por debajo se realiza «boxing» (boxing) y «unboxing» (unboxing) de valores primitivos a clases envoltorio (Integer, Double, etc.). Es cómodo, pero no siempre eficiente:
- Boxing — convertir un primitivo (int) en un objeto (Integer).
- Unboxing — la operación inversa: de objeto a primitivo.
Problema:
El boxing/unboxing añade operaciones y consumo de memoria. En las zonas calientes (código que se invoca con frecuencia) puede provocar una caída apreciable del rendimiento, especialmente si procesas grandes arrays de números.
Ejemplo:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().map(x -> x * 2).reduce(0, Integer::sum);
Aquí cada número es un objeto Integer, no un primitivo int.
2. Streams primitivos: IntStream, LongStream, DoubleStream
Para evitar el boxing/unboxing innecesario, Java dispone de streams primitivos:
- IntStream — para int
- LongStream — para long
- DoubleStream — para double
Trabajan solo con primitivos y no crean objetos envoltorio de más.
¿Cómo crear un stream primitivo?
Desde un array:
int[] arr = {1, 2, 3, 4, 5};
IntStream s = Arrays.stream(arr);
Con range/rangeClosed:
IntStream.range(0, 10) // 0..9
IntStream.rangeClosed(1, 5) // 1..5 incluidos
Generación de números aleatorios:
new Random().ints(5, 0, 100) // 5 int aleatorios de 0 a 99
Ejemplo: suma de los elementos de un array
int[] arr = {1, 2, 3, 4, 5};
int sum = Arrays.stream(arr).sum(); // 15
3. Conversión entre Stream<T> y streams primitivos
A veces tienes un stream normal y otras un primitivo. Para convertirlos se usan métodos específicos:
- mapToInt, mapToLong, mapToDouble — convierten un stream normal en uno primitivo.
- boxed() — convierte un stream primitivo de vuelta en un stream de objetos.
Ejemplo:
List<String> words = List.of("Java", "Stream", "API");
IntStream lengths = words.stream().mapToInt(String::length);
lengths.forEach(System.out::println); // 4 6 3
En sentido contrario:
IntStream ints = IntStream.range(1, 5);
Stream<Integer> boxed = ints.boxed();
Atención:
boxed() es la operación inversa: vuelve a crear objetos envoltorio (Integer, Double, etc.), y si necesitas el máximo rendimiento — evítala en los «puntos calientes».
4. Suma y resúmenes: sum, average, min, max, summaryStatistics
Los streams primitivos tienen métodos de agregación muy prácticos:
- sum() — suma de todos los elementos
- average() — media (devuelve OptionalDouble)
- min(), max() — mínimo y máximo (OptionalInt, OptionalLong, OptionalDouble)
- summaryStatistics() — devuelve un objeto con estadísticas completas (suma, media, mínimo, máximo, cantidad)
Ejemplo:
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
Comparación con Collectors:
Para streams normales se puede usar Collectors.summarizingInt, pero a menudo es menos eficiente que los métodos de streams primitivos:
List<Integer> nums = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = nums.stream().collect(Collectors.summarizingInt(x -> x));
5. Evitando el autoboxing: dónde importa y cómo medirlo
¿Dónde es crítico?
- En bucles y streams que procesan grandes volúmenes de números (por ejemplo, procesamiento de arrays, estadísticas, matemáticas, parseo de datos).
- En los «puntos calientes»: código que se ejecuta con frecuencia y afecta al rendimiento.
¿Por qué es importante?
- Cada vez que ocurre boxing, se crea un nuevo objeto envoltorio (Integer, Double, etc.).
- Esto incrementa la carga del recolector de basura (GC).
- En microbenchmarks la diferencia puede ser varias veces mayor.
¿Cómo medirlo?
Para una comparación precisa se utiliza el framework JMH (Java Microbenchmark Harness). Permite comparar de forma honesta el rendimiento del código con y sin boxing.
Ejemplo (pseudocódigo):
@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 segunda variante será más lenta debido a la creación constante de objetos Integer.
Conclusión:
Si te importa el rendimiento — ¡usa streams primitivos!
6. Cuándo los streams primitivos realmente ayudan y cuándo no merece la pena complicar
Usa streams primitivos si:
- Trabajas con arrays/colecciones grandes de números.
- Necesitas calcular rápidamente suma, media, mínimo, máximo.
- Escribes código donde importa cada milisegundo (por ejemplo, procesamiento en tiempo real).
No hace falta complicarse si:
- La colección es pequeña (decenas o unas pocas unidades de elementos).
- El código se vuelve demasiado complejo por las conversiones entre tipos.
- El rendimiento no es crítico (por ejemplo, procesamiento de entrada de usuario).
Ejemplo:
List<Integer> smallList = List.of(1, 2, 3);
int sum = smallList.stream().mapToInt(x -> x).sum(); // Se puede, pero un reduce normal también vale
Consejo: no conviertas el código en «jungla» por una optimización microscópica. Usa streams primitivos donde realmente esté justificado.
7. OptionalInt, OptionalDouble: extracción segura de resultados
Los métodos min(), max(), average() de los streams primitivos no devuelven simplemente un número, sino un «envoltorio» — OptionalInt, OptionalDouble, etc. Esto es necesario para manejar de forma segura streams vacíos (por ejemplo, si el array está vacío).
Ejemplo:
int[] arr = {};
OptionalInt min = Arrays.stream(arr).min();
if (min.isPresent()) {
System.out.println("Mínimo: " + min.getAsInt());
} else {
System.out.println("¡El array está vacío!");
}
Comparación con el Optional normal:
- OptionalInt — para int
- OptionalDouble — para double
- OptionalLong — para long
¿Por qué no devolver simplemente 0?
Porque el 0 puede ser un valor válido, y un stream vacío es una situación distinta.
Ejemplo con average:
double[] arr = {};
OptionalDouble avg = Arrays.stream(arr).average();
double result = avg.orElse(Double.NaN); // si está vacío — devolverá NaN
8. Práctica: ejemplos de uso de streams primitivos
Ejemplo 1: suma de los cuadrados de 1 a 1000
int sum = IntStream.rangeClosed(1, 1000)
.map(x -> x * x)
.sum();
System.out.println(sum);
Ejemplo 2: filtrado y conteo de números pares
int[] arr = {1, 2, 3, 4, 5, 6};
long count = Arrays.stream(arr)
.filter(x -> x % 2 == 0)
.count();
System.out.println("Números pares: " + count);
Ejemplo 3: comparación con un Stream<Integer> normal
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");
En volúmenes grandes la diferencia será apreciable.
9. Errores típicos al trabajar con streams primitivos
Error n.º 1: olvidar el boxing al convertir tipos.
Si usas boxed(), recuerda que vuelve a crear objetos envoltorio. No lo uses si no es necesario.
Error n.º 2: usar streams primitivos para objetos.
IntStream funciona solo con int, no con objetos. Si necesitas trabajar con objetos — usa un Stream<T> normal.
Error n.º 3: ignorar OptionalInt/OptionalDouble.
Si llamas a min(), max(), average() — comprueba siempre que hay resultado (isPresent()), de lo contrario obtendrás una excepción.
Error n.º 4: conversiones demasiado complejas entre Stream<T> e IntStream.
Si el código se vuelve ilegible por constantes mapToInt() → boxed() → mapToDouble(), quizá debas simplificar la lógica.
Error n.º 5: esperar una aceleración «mágica» en colecciones pequeñas.
Para listas pequeñas la diferencia entre Stream e IntStream es mínima. No compliques el código por un ahorro microscópico.
GO TO FULL VERSION