CodeGym /Cursos /JAVA 25 SELF /Streams primitivos e o custo do boxing

Streams primitivos e o custo do boxing

JAVA 25 SELF
Nível 33 , Lição 0
Disponível

1. Problema: por que Streams “normais” nem sempre são eficientes

Quando você trabalha com coleções de números em Java e usa streams “normais” (Stream<Integer>, Stream<Double>), por baixo dos panos ocorre “boxing” (empacotamento) e “unboxing” (desempacotamento) de valores primitivos em objetos wrapper (Integer, Double etc.). Isso é conveniente, mas nem sempre eficiente:

  • Boxing — transformação de um primitivo (int) em um objeto (Integer).
  • Unboxing — a operação inversa: de objeto para primitivo.

Problema:
Boxing/unboxing são operações extras e consomem memória. Em trechos “quentes” (chamados com frequência) do programa, isso pode levar a uma queda perceptível de desempenho, especialmente se você estiver processando grandes arrays de números.

Exemplo:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().map(x -> x * 2).reduce(0, Integer::sum);

Aqui, cada número é um objeto Integer, não um primitivo int.

2. Streams primitivos: IntStream, LongStream, DoubleStream

Para evitar boxing/unboxing desnecessários, o Java tem streams primitivos:

  • IntStream — para int
  • LongStream — para long
  • DoubleStream — para double

Eles funcionam apenas com primitivos e não criam objetos wrapper desnecessários.

Como criar um stream primitivo?

A partir de um array:

int[] arr = {1, 2, 3, 4, 5};
IntStream s = Arrays.stream(arr);

Usando range/rangeClosed:

IntStream.range(0, 10)          // 0..9
IntStream.rangeClosed(1, 5)     // 1..5 inclusivo

Geração de números aleatórios:

new Random().ints(5, 0, 100)    // 5 ints aleatórios de 0 a 99

Exemplo: somando os elementos de um array

int[] arr = {1, 2, 3, 4, 5};
int sum = Arrays.stream(arr).sum(); // 15

3. Conversão entre Stream<T> e streams primitivos

Às vezes você tem um stream “normal”, e às vezes um primitivo. Para converter, use métodos específicos:

  • mapToInt, mapToLong, mapToDouble — convertem um stream normal em um primitivo.
  • boxed() — transforma um stream primitivo de volta em um stream de objetos.

Exemplo:

List<String> words = List.of("Java", "Stream", "API");
IntStream lengths = words.stream().mapToInt(String::length);
lengths.forEach(System.out::println); // 4 6 3

No caminho inverso:

IntStream ints = IntStream.range(1, 5);
Stream<Integer> boxed = ints.boxed();

Atenção:
boxed() é a operação inversa; ela volta a criar objetos wrapper (Integer, Double etc.). Se você precisa de máxima performance, evite usá-la em trechos “quentes”.

4. Soma e resumos: sum, average, min, max, summaryStatistics

Streams primitivos têm métodos convenientes de agregação:

  • sum() — soma de todos os elementos
  • average() — média (retorna OptionalDouble)
  • min(), max() — mínimo e máximo (OptionalInt, OptionalLong, OptionalDouble)
  • summaryStatistics() — retorna um objeto com estatísticas completas (soma, média, mínimo, máximo, contagem)

Exemplo:

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

Comparação com Collectors:
Para streams normais, é possível usar Collectors.summarizingInt, mas isso geralmente é menos eficiente do que os métodos dos streams primitivos:

List<Integer> nums = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = nums.stream().collect(Collectors.summarizingInt(x -> x));

5. Evitando autoboxing: onde isso importa e como medir

Onde isso é crítico?

  • Em loops e streams que processam grandes quantidades de números (por exemplo, processamento de arrays, estatística, matemática, parsing de dados).
  • Em trechos “quentes” — código que é chamado com frequência e impacta o desempenho.

Por que isso importa?

  • Cada vez que ocorre boxing, um novo objeto wrapper (Integer, Double etc.) é criado.
  • Isso aumenta a pressão sobre o coletor de lixo (GC).
  • Em microbenchmarks, a diferença pode ser de várias vezes!

Como medir?
Para uma comparação precisa, use o framework JMH (Java Microbenchmark Harness). Ele permite comparar de forma justa o desempenho do código com e sem boxing.

Exemplo (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);
}

A segunda variante será mais lenta devido à criação constante de objetos Integer.

Conclusão:
Se o desempenho é importante — use streams primitivos!

6. Quando streams primitivos realmente ajudam e quando não vale a pena complicar

Use streams primitivos se:

  • Você trabalha com arrays/coleções grandes de números.
  • Você precisa calcular rapidamente soma, média, mínimo e máximo.
  • Você escreve código onde cada milissegundo importa (por exemplo, processamento de dados em tempo real).

Você pode não se preocupar se:

  • A coleção é pequena (dezenas ou poucos elementos).
  • O código fica complexo demais por causa das conversões entre tipos.
  • O desempenho não é crítico (por exemplo, processamento de entrada do usuário).

Exemplo:

List<Integer> smallList = List.of(1, 2, 3);
int sum = smallList.stream().mapToInt(x -> x).sum(); // Pode, mas um reduce normal também serve

Dica: não transforme o código em uma “selva” por causa de uma micro-otimização. Use streams primitivos onde isso realmente se justifica.

7. OptionalInt, OptionalDouble: extração segura de resultados

Os métodos min(), max(), average() nos streams primitivos retornam não apenas um número, mas um “wrapper” — OptionalInt, OptionalDouble etc. Isso é necessário para lidar com segurança com streams vazios (por exemplo, se o array estiver vazio).

Exemplo:

int[] arr = {};
OptionalInt min = Arrays.stream(arr).min();
if (min.isPresent()) {
    System.out.println("Mínimo: " + min.getAsInt());
} else {
    System.out.println("Array vazio!");
}

Comparação com Optional comum:

  • OptionalInt — para int
  • OptionalDouble — para double
  • OptionalLong — para long

Por que não simplesmente retornar 0?
Porque 0 pode ser um valor válido, e um stream vazio é uma situação diferente.

Exemplo com average:

double[] arr = {};
OptionalDouble avg = Arrays.stream(arr).average();
double result = avg.orElse(Double.NaN); // se vazio — retornará NaN

8. Prática: exemplos de uso de streams primitivos

Exemplo 1: soma dos quadrados de 1 a 1000

int sum = IntStream.rangeClosed(1, 1000)
                   .map(x -> x * x)
                   .sum();
System.out.println(sum);

Exemplo 2: filtragem e contagem 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);

Exemplo 3: comparação com um Stream<Integer> comum

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");

Em volumes grandes, a diferença será perceptível.

9. Erros típicos ao trabalhar com streams primitivos

Erro nº 1: esquecer o boxing ao converter tipos.
Se você usar boxed(), lembre-se de que isso volta a criar objetos wrapper. Não use sem necessidade.

Erro nº 2: usar streams primitivos para objetos.
IntStream funciona apenas com int, não com objetos. Se você precisa trabalhar com objetos, use um Stream<T> normal.

Erro nº 3: ignorar OptionalInt/OptionalDouble.
Se você chama min(), max(), average(), sempre verifique se há resultado (isPresent()), caso contrário você terá uma exceção.

Erro nº 4: conversões complexas demais entre Stream<T> e IntStream.
Se o código se torna ilegível por causa de constantes mapToInt()boxed()mapToDouble(), talvez valha a pena simplificar a lógica.

Erro nº 5: esperar um “milagre” de aceleração em coleções pequenas.
Para listas pequenas, a diferença entre Stream e IntStream é mínima. Não complique o código por uma economia microscópica.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION