CodeGym /Cursos /JAVA 25 SELF /Streams primitivos y el coste del boxing

Streams primitivos y el coste del boxing

JAVA 25 SELF
Nivel 33, Lección 0
Disponible

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.

1
Tarea
JAVA 25 SELF, nivel 33, lección 0
Bloqueada
Cálculo de los ingresos diarios de la tienda 💰
Cálculo de los ingresos diarios de la tienda 💰
1
Tarea
JAVA 25 SELF, nivel 33, lección 0
Bloqueada
Análisis de la longitud de los hechizos para la Academia de Conocimientos Ocultos ✨
Análisis de la longitud de los hechizos para la Academia de Conocimientos Ocultos ✨
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION