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.
GO TO FULL VERSION