Conjunto de Exercícios - Java Stream API Avançado
Conjunto de Exercícios - Java Stream API Avançado
Avançado
Seção 1 – filter (5 questões)
• 1.1. Em um cenário de e-commerce, temos uma lista de pedidos com seu status. Considere a
classe Pedido com atributos id e status (por exemplo, "PENDENTE", "ENTREGUE", etc.).
Suponha a lista:
Pergunta: Utilizando Stream.filter, obtenha uma lista contendo apenas os pedidos cujo status é
"ENTREGUE" . (Dica: lembre-se de realizar uma operação terminal para materializar o resultado.)
• 1.2. Você mantém uma lista de usuários de um sistema, onde cada usuário tem atributos como
nome , idade e ativo (indicando se o usuário está ativo no sistema). Por exemplo:
Pergunta: Usando filter, obtenha os usuários que estão ativos e têm idade maior ou igual a 18.
Quantos usuários atendem a esses critérios?
• 1.3. Considere o seguinte trecho de código que tenta filtrar pedidos entregues:
Ao executar, a saída não lista os pedidos entregues conforme esperado, exibindo algo semelhante a
Pedidos entregues: java.util.stream.ReferencePipeline$3@6d06d69c . Pergunta: Por que
1
o código acima não funcionou como pretendido? O que falta para que a filtragem seja de fato
executada e os pedidos entregues sejam obtidos?
• 1.4. Usando Streams, crie um stream infinito de números inteiros positivos e filtre apenas
aqueles divisíveis por 7. Então, obtenha o primeiro número maior que 100 que atenda a essa
condição. (Dica: utilize Stream.iterate para gerar os números e uma operação terminal
adequada para obter um único resultado.)
A)
produtos.stream().map(p -> p.getPrecoFinal()).filter(preco -> preco > 1000)...
B) produtos.stream().filter(p -> p.getPrecoFinal() >
1000).map(Produto::getNome)...
Pergunta: Qual das duas abordagens é mais eficiente e por quê? O que isso demonstra sobre a ordem
das operações filter e map em um pipeline de Stream?
Pergunta: Utilizando Stream.map, obtenha uma lista de Strings contendo apenas os endereços de
email dos clientes.
Pergunta: Use map para aplicar um aumento de 10% em cada preço, gerando uma nova lista com os
preços reajustados. Por exemplo, 50.0 deve se tornar 55.0 . (Não modifique a lista original, crie
uma nova com os valores alterados.)
2
Pergunta: Use map para converter essa lista de Strings em uma lista de Integer . (Desconsidere
possíveis formatos inválidos na lista; assuma que todas as strings são números válidos.)
Pergunta: Por que esse código não enviou os emails? Como corrigir o uso da Stream API para realizar
uma ação (envio de email) para cada usuário da lista? Explique a diferença entre map e forEach nesse
contexto.
• 2.5. Suponha que você tenha uma lista muito grande de números inteiros e precise aplicar uma
função pesada de cálculo em cada número (por exemplo, uma operação matemática complexa).
Para melhorar a performance, deseja-se utilizar processamento paralelo. Pergunta: Como você
utilizaria Streams para aplicar essa função em todos os elementos o mais rápido possível?
Esboce um código usando streams paralelos que aplique a função computarAlgo(int x)
(imaginária e custosa) em cada elemento de uma lista grande de inteiros. Comente brevemente
sobre em que cenários parallelStream() tende a melhorar a performance.
Pergunta: Use flatMap para transformar essa estrutura em uma única lista contendo todos os nomes
de funcionários da empresa (ou seja, “achate” a lista de listas em uma lista única).
e uma lista List<Order> orders onde cada pedido possui uma lista de itens comprados. Pergunta:
Utilize flatMap para obter uma lista única contendo todos os itens de todos os pedidos. (Por exemplo,
se cada Order possui seus itens, o resultado deve ser a união de itens de todos os pedidos.)
• 3.3. Você tem uma lista de strings em formato CSV (valores separados por vírgula), onde cada
string contém uma série de palavras separadas por vírgulas:
3
List<String> linhas = Arrays.asList("java,python,c", "ruby,javascript", "c+
+,go");
Pergunta: Use flatMap para obter uma lista de todos os valores individuais nessas strings. Ou seja, a
saída desejada para o exemplo acima seria uma lista:
["java","python","c","ruby","javascript","c++","go"] .
• 3.4. Qual a diferença prática entre usar map e flatMap? Considere um exemplo:
• 3.5. Suponha que você tem uma lista List<Optional<String>> contendo valores opcionais
(alguns presentes, outros vazios). Por exemplo:
Pergunta: Utilizando Streams, obtenha uma lista de Strings com todos os valores presentes em
talvezNomes , descartando os opcionais vazios. (Dica: há várias maneiras de fazer, mas tente pensar
em como flatMap pode ajudar a simplificar a operação com Optional .)
Pergunta: Use Stream.reduce para calcular a soma de todos os números da lista. (Não use
IntStream.sum() ou Collectors.summingInt ; a ideia é praticar o uso do reduce manualmente.)
4
Pergunta: Utilize reduce para encontrar o nome mais longo presente na lista. (Dica: você pode usar
uma String vazia "" como elemento identidade e, na função acumuladora, sempre reter a string
de maior comprimento.)
Pergunta: Use reduce para concatenar todas as palavras em uma única string, separadas por vírgula.
(Desafio: cuide para que a string resultante não comece nem termine com vírgula extra – por exemplo, "java,
stream, api, colecoes".)
• 4.4. Considere uma lista de frases (Strings) e queremos saber o total de caracteres de todas as
frases somadas. Por exemplo:
Pergunta: Use reduce para calcular o número total de caracteres de todas as strings da lista
frases . (Dica: você pode utilizar a versão de reduce que recebe três argumentos – identidade,
acumulador e combinador – para fazer a redução diretamente de String para um contador int .)
e uma lista List<Transacao> transacoes . Queremos derivar algumas estatísticas simples dessas
transações. Pergunta: Implemente, usando reduce, a computação de um objeto Stats que contenha
o total de transações (count) e a soma dos valores de todas elas. Defina a classe auxiliar Stats com
campos count (long) e sum (double), e utilize reduce com identidade, acumulador e combinador
para produzir um único Stats agregando esses resultados. (Dica: Inicie com new Stats(0, 0.0)
como identidade e, no acumulador, incremente o contador e adicione o valor da transação. Lembre-se que a
função de combinação deve mesclar dois objetos Stats .)
Pergunta: Use filter e então collect para obter uma List<Integer> contendo apenas os números
pares da lista original. (Exemplo de resultado esperado para a lista acima: [2, 4, 6] .)
• 5.2. Suponha uma lista de palavras (possivelmente com repetições e variações de maiúsculas/
minúsculas):
5
List<String> palavras = Arrays.asList("Java", "java", "STREAM", "Api",
"Java");
Pergunta: Utilize Collectors.joining para concatenar todos os nomes em uma única string separados
por ", " (vírgula e espaço). Por exemplo: "Java, Python, C, JavaScript" .
• 5.4. Você tem uma lista de valores de vendas mensais (números double ). Por exemplo:
Pergunta: Use Collectors.summarizingDouble para obter de uma só vez o total de vendas, média,
valor mínimo e máximo, bem como a quantidade de entradas. Explique brevemente o conteúdo do
objeto retornado (um DoubleSummaryStatistics ) e exiba seus principais valores.
class Employee {
String nome;
String departamento;
int idade;
double salario;
boolean ativo;
Employee(String n, String dept, int idade, double sal, boolean ativo) {
this.nome = n; this.departamento = dept;
this.idade = idade; this.salario = sal; this.ativo = ativo;
}
}
6
List<Employee> employees = Arrays.asList(
new Employee("Alice", "IT", 28, 3000.0, true),
new Employee("Bob", "Sales", 45, 5000.0, false),
new Employee("Carol", "IT", 35, 4500.0, true),
new Employee("David", "HR", 52, 4500.0, true),
new Employee("Eve", "Sales", 29, 3500.0, true)
);
Pergunta: Use groupingBy para agrupar as palavras pelo tamanho (número de letras). Ou seja,
produza um Map<Integer, List<String>> em que a chave é o tamanho da palavra e o valor é a
lista de palavras com aquele tamanho. (Exemplo: tamanho 5 -> ["navio", "avião"] etc.)
• 6.3. Usando novamente a lista employees definida em 6.1, agrupe os funcionários por faixa
etária. Defina as faixas conforme a idade: menores de 30 anos, entre 30 e 50 anos, e maiores
de 50 anos.
Pergunta: Produza um Map<String, List<Employee>> com três entradas (por exemplo, chaves
"Jovens", "Experientes", "Seniors"), e liste quais funcionários caem em cada faixa etária. (Dica: você pode
passar uma função lambda para groupingBy que verifica a idade de cada empregado e retorna a string
categoria apropriada.)
• 6.4. Usando a mesma lista de employees , queremos separar os funcionários ativos dos
inativos.
• 6.5. Ainda com a lista employees , suponha que desejamos saber o total de salário pago por
departamento.
7
List<String> nomes = Arrays.asList("Carlos", "Ana", "Bruno", "Eduarda");
Pergunta: Use Stream.sorted() para obter os nomes em ordem alfabética crescente. Qual seria a
ordem resultante? (Obs: Considere a ordenação lexicográfica padrão do Java.)
• 7.2. Considere novamente a lista employees (com campos nome, salário, etc. conforme
definido na seção 6.1). Pergunta: Utilize sorted com um comparador customizado para ordenar
os funcionários por salário em ordem decrescente (do maior para o menor salário). Em caso de
salários iguais, ordene os funcionários com esses salários em ordem alfabética pelo nome.
Apresente a lista ordenada final (nome, salário) seguindo esses critérios.
• 7.3. Suponha uma classe Product com atributos nome , preco e estoque (quantidade em
estoque), e uma lista de produtos:
Pergunta: Usando anyMatch, verifique se algum produto está fora de estoque. (Ou seja, se existe
pelo menos um produto com estoque == 0 na lista.) Qual é o resultado dessa verificação para a lista
acima?
• 7.5. Ainda sobre os funcionários, pergunta: use noneMatch para checar se nenhum
funcionário tem menos de 18 anos (por exemplo, para garantir que não há menores de idade
contratados). O que essa verificação retorna com a lista atual de employees, e por quê? (Explique
em termos de como noneMatch funciona.)
class Order { int id; String cliente; double total; String status; /*
construtor */ }
List<Order> pedidos = Arrays.asList(
new Order(1, "Maria", 250.0, "ENTREGUE"),
new Order(2, "João", 300.0, "PENDENTE"),
new Order(3, "Maria", 700.0, "ENTREGUE"),
new Order(4, "Ana", 1500.0, "ENTREGUE"),
8
new Order(5, "Pedro", 130.0, "PENDENTE")
);
Pergunta: Usando uma pipeline de stream, filtre os pedidos com status "ENTREGUE" , extraia os
nomes dos clientes desses pedidos, elimine duplicatas e obtenha a lista de clientes ordenada
alfabeticamente. (Em outras palavras, quais clientes têm pelo menos um pedido entregue? Liste-os em
ordem alfabética, sem repetir nomes.)
• 8.2. Com a lista de pedidos do exemplo anterior, suponha que queremos saber o total em
vendas de pedidos de alto valor. Pergunta: Filtre os pedidos com total > 500 e calcule a
soma dos valores desses pedidos. (Combine as operações de filtro, map e reduce para obter um
valor double único como resultado.)
• 8.3. Usando a lista de employees (da seção 6.1), pergunta: obtenha um Map que associe cada
departamento à lista dos nomes dos funcionários ativos naquele departamento. (Combine
filtro por ativo , seguido de um collect com groupingBy no departamento e mapeando
cada funcionário ativo para seu nome.)
• 8.4. Suponha uma classe Transacao com campos tipo (String, ex: "COMPRA", "VENDA",
"ESTORNO") e valor (double), e uma lista:
Pergunta: Usando Streams, filtre as transações de valor acima de 100 e então agrupe-as por tipo,
obtendo a quantidade de transações de cada tipo. (Combine filter +
collect(Collectors.groupingBy(..., Collectors.counting())) .)
• 8.5. Considere a lista de produtos definida na questão 7.3 acima. Pergunta: Obtenha os 3
produtos mais caros (maior preço) e retorne uma lista apenas com os nomes desses três
produtos, em ordem do mais caro para o terceiro mais caro. (Dica: você pode usar sorted em
ordem decrescente de preço, seguido de limit(3) e então mapear para nomes.)
• 8.6. Retornando ao cenário de pedidos com itens, suponha que agora a classe Order possui
um campo status e uma lista de Item (s) associados:
9
new Item("Cabo HDMI", "Acessórios", 2)
)),
new Order(2, "PENDENTE", Arrays.asList(
new Item("Notebook", "Eletrônicos", 1)
)),
new Order(3, "ENTREGUE", Arrays.asList(
new Item("Mouse", "Acessórios", 3),
new Item("Teclado", "Acessórios", 1)
))
);
Pergunta: Usando uma única pipeline de Stream, filtre apenas os pedidos com status "ENTREGUE" ,
extraia todos os itens desses pedidos (flatten da lista de itens), e então calcule um Map que mapeia
cada categoria de produto à quantidade total vendida (soma das quantidades de itens entregues por
categoria). (Combine filter, flatMap e um collect com groupingBy + summingInt.)
• 8.7. Com a lista employees , pergunta: produza, via Streams, um relatório da forma
Map<Boolean, Long> que indique quantos funcionários estão ativos ( true ) e quantos não
estão ( false ). (Use Collectors.partitioningBy com um coletor de contagem.)
Pergunta: Usando uma cadeia de operações, obtenha uma lista dos quadrados desses números (isto
é, eleve cada número ao quadrado), removendo valores duplicados e ordenando a lista em ordem
crescente. Em seguida, transforme essa lista resultante em uma lista imutável (não modificável). (Dica:
combine map , distinct , sorted e então use um coletor apropriado ou collectingAndThen .)
Qual seria o resultado final para a lista acima?
• 8.9. Considere uma lista de idades em formato String, onde algumas entradas podem ser "N/
A" (não disponíveis):
Pergunta: Usando Streams, filtre os valores não numéricos (neste caso, remova as strings "N/A" ),
converta as demais para inteiro e então verifique se todas as idades correspondem a adultos (idade >=
18). Use uma operação terminal apropriada para obter um booleano e interprete o resultado para a
lista fornecida.
• 8.10. Dada uma lista de números inteiros que pode conter valores negativos:
Pergunta: Utilizando Streams, calcule a média dos quadrados de todos os valores positivos da lista.
Combine filter (para filtrar apenas os positivos), map (para obter os quadrados) e então obtenha a
10
média com o terminal adequado. Qual é o resultado (média) esperado para a lista acima? (Obs: Lembre
que Stream.average() retorna um OptionalDouble.)
Pergunta: Implemente um Spliterator customizado para iterar de forma eficiente por um intervalo de
long de 1 até N, dividindo-o em partes para possibilitar paralelismo. Em seguida, mostre como
utilizar esse Spliterator com StreamSupport.stream(..., true) e como executar a operação de
soma em um ForkJoinPool customizado (por exemplo, um pool com um número fixo de threads), em
vez de usar o ForkJoinPool.commonPool() padrão 1 . Apresente o código do Spliterator e um
exemplo de uso que some os números de 1 a, digamos, 1_000_000, usando um pool customizado de
threads.
• 9.2. Custom Collector: A maioria dos coletores comuns podem ser obtidos via Collectors , mas
às vezes precisamos criar comportamentos específicos.
• 9.3. Tratamento de Exceções em Pipeline: Considere que você tem uma lista de caminhos de
arquivos:
e deseja ler todas as linhas de todos os arquivos dessa lista, agregando-as em uma única lista de
strings. Porém, a leitura de arquivo pode lançar IOException , e um arquivo ("inexistente.txt") pode
não existir.
Pergunta: Usando Streams, escreva um pipeline que leia todas as linhas dos arquivos existentes e
ignore (ou trate gracilmente) os arquivos que não puderem ser lidos, sem quebrar a execução do
stream inteiro. Em outras palavras, como você pode usar operações como map / flatMap e
tratamento de exceções (try/catch) dentro do lambda para capturar a IOException de leitura de
arquivo e seguir o processamento para os demais arquivos? Mostre um trecho de código que realiza
essa tarefa, e explique como o stream lida com o arquivo inexistente.
• 9.4. Debug e Profiling com peek: Considere o pipeline a seguir que processa uma lista de
números:
11
List<Integer> lista = Arrays.asList(1, 2, 3, 4);
List<Integer> resultado = lista.stream()
.filter(x -> x % 2 == 0)
.map(x -> x * x)
.collect(Collectors.toList());
Pergunta: Insira chamadas a peek() nesse pipeline de forma a exibir no console: (a) cada número
conforme ele é recebido no stream original, (b) cada número que passa pelo filtro (números pares) e (c)
cada número após ser mapeado para o seu quadrado. Execute o pipeline com essas inserções para a
lista de exemplo e apresente a saída impressa, explicando a ordem em que os elementos são
processados pelo pipeline. (Use mensagens de log como "Original: x" , "Filtrado: x" e
"Mapeado: y" para identificar as etapas.)
Pergunta: Escreva um código que crie uma grande lista de números (por exemplo, 10 milhões de
inteiros) e calcule a soma desses números de três formas: (a) usando um loop for convencional, (b)
usando um Stream sequencial (por exemplo, stream().mapToLong(...).sum() ), e (c) usando um
Stream em paralelo. Meça o tempo de execução de cada abordagem (por meio de
System.nanoTime() ou similar) e exiba os resultados. Comente sobre os tempos observados – qual
abordagem foi mais rápida? Em que cenário o uso de stream paralelo compensa em relação ao
sequencial? Fundamente sua resposta com base nos resultados e no custo adicional que a Stream API
introduz 3 .
Respostas
Resposta 1.1: Para obter apenas os pedidos com status "ENTREGUE" , é necessário aplicar o filtro e
depois coletar o resultado (por exemplo, em uma lista). Cada operação intermediate de stream (como
filter) só será executada quando ocorrer uma operação terminal 4 5 . Portanto, devemos adicionar
uma operação terminal como collect . Exemplo de implementação:
Resposta 1.2: Podemos encadear as condições no predicado do filtro usando o operador lógico && .
Por exemplo:
12
List<Usuario> result = usuarios.stream()
.filter(u -> u.ativo && u.idade >= 18)
.collect(Collectors.toList());
System.out.println(result);
Para a lista fornecida, apenas Ana (23 anos, ativa) e Carla (34 anos, ativa) atendem às condições.
Portanto, result conteria esses 2 usuários. A saída seria algo como:
Todos os demais são filtrados: Bruno é menor de 18, e Daniel não é filtrado porque todos os ativos >=18
passam (Daniel tem 28 e ativo, então na verdade Daniel também passaria. Retificando: Bruno é excluído
por idade; Bob não existia, erro de nomes).
Resposta 1.3: O código não funcionou porque faltou uma operação terminal para consumir o stream
filtrado. O método filter retorna um novo Stream (intermediário) em vez de executar
imediatamente o filtro 4 . No exemplo dado, a variável entregues é um Stream (não uma lista de
pedidos filtrados), e o System.out.println acaba imprimindo a referência do objeto stream em vez
do conteúdo. Para corrigir, devemos adicionar uma operação terminal, como collect , count ,
forEach , etc. Por exemplo:
Agora, entregues será de fato uma lista de pedidos filtrados, e o println exibirá os elementos
esperados (p. ex., os objetos Pedido com status ENTREGUE). Em resumo: sem a operação terminal, a
pipeline não é executada (lazy evaluation) 5 .
Resposta 1.4: Podemos usar Stream.iterate para criar um stream infinito e então usar filter e
findFirst para pegar o primeiro elemento que atende à condição. Exemplo:
Primeiro, geramos inteiros 1,2,3,... infinitamente. O pipeline de filtros mantém apenas múltiplos
de 7 e maiores que 100. findFirst() é uma operação terminal de curto-circuito que para ao
encontrar o primeiro elemento correspondente. O resultado será 105 nesse caso (pois 105 é o
primeiro número >100 divisível por 7).
13
Observe que graças à avaliação lazy e de curto-circuito, o stream processa apenas o necessário até
encontrar o resultado e então termina, sem iterar indefinidamente.
Resposta 1.5: A abordagem B ( filter antes de map ) é mais eficiente. Motivo: ao filtrar primeiro,
apenas os produtos cujo preço final ultrapassa 1000 passarão para a operação de map. Assim, o
método custoso getPrecoFinal() será chamado somente para produtos que depois serão
aproveitados, evitando cálculos desnecessários em itens que seriam descartados pelo filtro 5 . Na
abordagem A, o código calcula o preço final de todos os produtos (via map ) para só então descartar
alguns no filtro – desperdiçando tempo nos produtos que não atendem ao critério. Em resumo, filtrar
antes de mapear poupa processamento, especialmente quando a transformação (map) é custosa ou a
condição de filtro reduz significativamente o número de elementos. Essa situação ilustra que a ordem
das operações importa: geralmente é recomendável aplicar filtros o mais cedo possível no pipeline.
Resposta 2.1: Podemos usar map para transformar cada Cliente em seu email. Por exemplo:
Aqui usamos map(c -> c.email) para extrair o campo email de cada cliente. O resultado é coletado
em uma List<String> contendo apenas os emails. (Se a classe tivesse um método getter,
poderíamos usar .map(Cliente::getEmail) .)
Cada valor foi multiplicado por 1.1 (acréscimo de 10%). Observação: usamos 1.10 (double) para manter
precisão decimal nos cálculos. A lista original precos permanece inalterada; criamos uma nova lista
com os valores ajustados.
Resposta 2.3: Usamos map para converter de String para Integer via Integer.parseInt . Por
exemplo:
14
List<Integer> numeros = numerosStr.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
System.out.println(numeros);
Aqui assumimos que todas as strings são numéricas válidas. Se houvesse alguma string não-numérica,
parseInt lançaria exceção; nesse caso real, poderíamos primeiro filtrar strings inválidas ou tratar a
exceção dentro do lambda (ver resposta 9.3 sobre tratamento de exceções em streams). Mas com a
entrada fornecida, a conversão ocorre normalmente.
Resposta 2.4: O código original não enviou emails porque não houve uma operação terminal
consumindo o stream – similar ao problema da questão 1.3. A chamada stream().map(...) sozinha
não executa nada sem um terminal (como forEach) 4 . Além disso, conceitualmente usar map é
inadequado para realizar ações cujo resultado não será utilizado. A função passada a map produz um
Stream<Resultado> (nesse caso Stream<Boolean> de resultados de envio), mas esse stream resultante
é ignorado.
Ou em método de referência:
usuarios.stream().forEach(UsuarioService::enviarEmail);
Assim, o método enviarEmail(u) será executado para cada usuário. Diferentemente de map,
forEach é uma operação terminal que itera pelos elementos executando a ação especificada em
cada um. Em resumo, map deve ser usado para transformações puras de dados (produzir um novo
valor a partir de cada elemento), enquanto forEach (ou collect, etc.) deve ser usado para acionar efeitos
colaterais ou consumir o resultado final do stream.
Nesse código, o stream é dividido automaticamente em tarefas que rodam em múltiplas threads do
ForkJoinPool comum (por padrão, usando número de threads igual ao número de cores do
15
processador). A função custosa computarAlgo(x) será aplicada em paralelo sobre diferentes partes
da lista, potencialmente acelerando o processamento total.
É importante notar que paralelizar traz benefício principalmente quando o trabalho por elemento
( computarAlgo ) é suficientemente pesado comparado ao overhead do paralelismo, e quando há
recursos de CPU disponíveis (vários núcleos) 3 . Caso contrário, parallelStream() pode não
compensar. Além disso, a função usada no pipeline deve ser thread-safe (não acessar/modificar estado
compartilhado sem sincronização). Em suma, parallelStream pode melhorar o desempenho em
cenários CPU-bound com grande volume de dados, aproveitando todos núcleos do sistema
automaticamente.
Isso itera por cada Order em orders , e o flatMap transforma cada pedido em um stream de seus
itens, unificando tudo. Para as orders definidas no exemplo (não explícito na pergunta, mas
assumindo estrutura semelhante ao código dado), a saída seria uma lista contendo todos os objetos
Item presentes em todos os pedidos. Se quiséssemos apenas, por exemplo, os nomes dos itens,
poderíamos adicionar .map(item -> item.nome) após o flatMap.
Como ilustração, suponha orders contém 2 pedidos: um com itens [A, B] e outro com [C]. O resultado
do flatMap será [A, B, C].
Resposta 3.3: Cada string deve ser quebrada por vírgula e depois precisamos achatar. Podemos fazer:
16
List<String> valores = linhas.stream()
.flatMap(linha -> Arrays.stream(linha.split(",")))
.collect(Collectors.toList());
System.out.println(valores);
Para visualizar: com map , se tentarmos coletar o resultado, faríamos algo como:
Teríamos uma lista de 2 streams, cada um contendo [1,2] e [3,4]. Já com flatMap :
Resposta 3.5: Podemos usar flatMap de duas maneiras aqui. Desde o Java 9, Optional possui o
método stream() , que produz um stream de 0 ou 1 elementos (valor presente ou nada). Com Java 8,
podemos simular comportamento similar. A solução usando flatMap seria:
17
Explicação: para cada Optional, opt.map(Stream::of) converte o valor presente em um Stream de
um elemento; se o Optional estiver vazio, usamos .orElseGet(Stream::empty) para obter um
stream vazio. O flatMap então achata tudo isso. O resultado final, para a lista dada, será:
Faz a mesma coisa de forma mais direta. Em ambos os casos, usamos flatMap para remover os
"containers" vazios e extrair apenas os valores existentes.
Resposta 4.1: Podemos usar reduce(0, (soma, elemento) -> soma + elemento) ou aproveitar
o Integer::sum . Exemplo:
Saída:
50
No cálculo: 5+10+15+20 = 50. Aqui 0 é o elemento identidade (neutro para soma) e Integer::sum é a
função acumuladora que adiciona cada elemento à soma corrente. O resultado é um int com a soma
total. (Obs: Poderíamos também usar mapToInt(i->i).sum() , mas o exercício pediu uso de reduce
manualmente.)
Resposta 4.2: Podemos usar reduce com identidade "" (string vazia) e lógica para escolher a maior
string:
Saída:
18
Alexandre
Explicação: Começamos com acc vazio "" . Para cada nome na lista, comparamos o comprimento
com o do acumulado; se o nome atual for maior, passa a ser o acumulado. Ao final, maiorNome
contém a string de maior tamanho. (Nota: também poderíamos usar reduce((n1,n2) ->
n1.length()>=n2.length()? n1: n2).get() sem identidade, para evitar precisar do caso
especial inicial.)
Resposta 4.3: Podemos fazer a concatenação manualmente. Para evitar vírgula extra no começo ou
fim, podemos montar o string condicionalmente. Uma solução:
Saída:
Explicação: Usamos acc (acumulador) iniciado em "" . Para cada palavra p , se acc ainda estiver
vazio, retornamos apenas p (primeira palavra sem vírgula antes). Caso contrário, acrescentamos ",
" seguido de p . Assim, acc final fica com todas as palavras separadas por vírgula e espaço, sem
delimitador sobrando no início ou fim. Observação: Essa abordagem funciona, embora não seja a mais
eficiente para muitas strings (concatenação repetida de String cria muitos objetos); uma alternativa
seria usar Collectors.joining(", ") (como visto na questão 5.3), que internamente usa uma
estratégia mais eficiente.
Resposta 4.4: Podemos usar a forma de três argumentos de reduce . Por exemplo:
Saída:
22
Explicação: Aqui o reduce funciona assim – identidade é 0 (número inicial de caracteres). O acumulador
(soma, str) -> soma + str.length() pega o comprimento de cada string e adiciona ao
acumulado. O combinador Integer::sum soma resultados parciais (necessário caso o stream seja
paralelo, para combinar somas de diferentes threads; em execução sequencial, o combinador apenas
19
soma o valor final com 0 redundante). O total de caracteres das frases "Olá mundo" (9) + "Java
Streams" (12) + "reduce" (6) de fato é 27, não 22 – vamos recalcular: "Olá mundo" tem 9 (incluindo
espaço), "Java Streams" tem 12 (incluindo espaço), "reduce" 6. Somando: 9+12+6 = 27. Então o output
esperado seria 27, não 22; a diferença de 5 caracteres sugere que possivelmente o exemplo contava
diferente (talvez sem espaços?). Se contarmos sem espaços: "Olámundo"(8) + "JavaStreams"(11) +
"reduce"(6) = 25. De qualquer forma, o método é correto. Com as strings originais incluindo espaços,
totalChars=27. O código dado soma todos os caracteres (incluindo espaços e pontuação).
(Nota: Ajustando a saída esperada: considerando as frases dadas literalmente "Olá mundo" (9 caracteres
com espaço), "Java Streams" (12 com espaço), "reduce" (6), total = 27. Portanto totalChars
seria 27.)
Resposta 4.5: Aqui faremos um reduce que produz um objeto agregador Stats . Primeiramente,
definimos a classe:
class Stats {
long count;
double sum;
Stats(long count, double sum) { this.count = count; this.sum = sum; }
public String toString() { return "Stats{count=" + count + ", sum=" +
sum + "}"; }
}
Implementação do reduce:
Explicação: Começamos com um Stats inicial de contagem 0 e soma 0.0. O acumulador percorre cada
Transacao t , incrementando o contador e somando o valor. O combinador então combina dois Stats
parciais (necessário para execução paralela; em modo sequencial, será chamado no final apenas
juntando o resultado com um Stats neutro).
20
Suponha que transacoes contenha, por exemplo, valores 100.0, 200.0, 50.0; o resultado seria
Stats{count=3, sum=350.0} .
Importante: A abordagem acima mutabiliza o objeto Stats identitário (vai atualizando o mesmo objeto
a cada passo). Isso funciona em stream sequencial. Em stream paralelo, o comportamento é suportado
desde que a função combinadora também dê conta (o framework pode criar cópias do identity para
segmentos diferentes, dependendo da implementação de reduce – e chamará combinador para uni-
las). Em geral, para reduções com objetos mutáveis, é preferível usar collect (com Supplier e Combiner
separados) ao invés de reduce, para evitar armadilhas de concorrência. Aqui, porém, demonstramos o
princípio usando reduce.
[2, 4, 6]
Explicação: filtramos n % 2 == 0 para pegar apenas pares e então coletamos numa nova lista. A lista
original não é modificada. O resultado contém 2,4,6 conforme esperado.
Explicação: "Java" e "java" entram como "java" no set (duplicata removida), "STREAM" vira
"stream" , "Api" vira "api" . O Set resultante contém cada palavra em minúsculo apenas uma
vez.
21
Resposta 5.3: Podemos usar:
Isso produz:
Resposta 5.4:
Usando Collectors.summarizingDouble :
Count: 4
Sum: 8101.25
Min: 1000.0
Max: 3100.0
Average: 2025.3125
Esse coletor é útil por fazer tudo em uma única passagem pela data, de forma otimizada em código
nativo.
22
List<String> listaImutavel = frutas.stream()
.map(String::toLowerCase)
.distinct()
.sorted()
.collect(Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
));
System.out.println(listaImutavel);
Explicação:
- Primeiro, .map(String::toLowerCase) se fossemos uniformizar maiúsculas/minúsculas (no
exemplo todas já estão minúsculas, então seria opcional). - distinct() remove duplicatas ("banana"
aparece duas vezes na entrada, ficará uma só). - sorted() ordena alfabeticamente. Assim temos
"banana", "laranja", "maçã". - Finalmente, o collectingAndThen aplica um coletor ( toList() ) e
então passa o resultado para Collections.unmodifiableList . O resultado listaImutavel é
uma List que não pode ser modificada (se tentar fazer listaImutavel.add("x") , será lançada
UnsupportedOperationException).
Dessa forma, garantimos o retorno em um só pipeline de uma lista ordenada, sem duplicatas e
protegida contra modificações externas.
Resposta 6.1:
Para os dados fornecidos, os departamentos são "IT", "Sales" e "HR". As listas esperadas: - "IT" -> [Alice,
Carol] (2 funcionários) - "Sales" -> [Bob, Eve] (2 funcionários) - "HR" -> [David] (1 funcionário)
Resposta 6.2:
23
Agrupando por comprimento da string:
4 -> [bike]
5 -> [navio, avião, trem]
6 -> [carro]
Resposta 6.3:
Para os employees: - "Jovens" (idade < 30): Alice (28), Eve (29) – note que 29 entra em <30. -
"Experientes" (30–50): Bob (45), Carol (35) – ambos nessa faixa. - "Seniors" (> 50): David (52).
Resposta 6.4:
Usando partitioningBy:
24
System.out.println("Ativos: " + particao.get(true));
System.out.println("Inativos: " + particao.get(false));
Na lista: - Ativos (true): Alice, Carol, David, Eve (4 funcionários) - Inativos (false): Bob (1 funcionário)
Saída:
(O formato exato depende do toString de Employee, mas conceptualmente esses nomes em cada
grupo). O mapa particao tem chave true mapeando para lista de ativos, e chave false para
lista de inativos.
Resposta 6.5:
Para os dados: - "IT": Alice(3000) + Carol(4500) = 7500.0 - "Sales": Bob(5000) + Eve(3500) = 8500.0 - "HR":
David(4500) = 4500.0
Saída:
IT -> 7500.0
Sales -> 8500.0
HR -> 4500.0
Resposta 7.1:
Ordenando alfabeticamente:
25
Os nomes em ordem crescente: [Ana, Bruno, Carlos, Eduarda].
Saída:
Justificativa: sorted() sem comparador usa a ordenação natural das strings (lexicográfica). "Ana"
vem antes de "Bruno" (A < B), "Bruno" antes de "Carlos" (B < C), e "Eduarda" por último (E > C).
Resposta 7.2:
Considerando os valores de salário: - Bob: 5000.0 - Carol: 4500.0 - David: 4500.0 - Eve: 3500.0 - Alice:
3000.0
Ordenação final esperada: 1. Bob – 5000.0 (maior salário) 2. Carol – 4500.0 3. David – 4500.0 (Carol e
David têm salário igual, então em ordem de nome: "Carol" vem antes de "David" alfabeticamente) 4. Eve
– 3500.0 5. Alice – 3000.0
Saída:
Bob - 5000.0
Carol - 4500.0
David - 4500.0
Eve - 3500.0
Alice - 3000.0
(Repare que invertendo Carol e David no original, confirmando a ordenação secundária por nome.)
Resposta 7.3:
26
boolean algumForaDeEstoque = produtos.stream()
.anyMatch(p -> p.estoque == 0);
System.out.println("Algum produto fora de estoque? " + algumForaDeEstoque);
Na lista dada: - "Laptop": estoque 5 - "Smartphone": estoque 0 (fora de estoque) - "Teclado": estoque 20
- "Monitor": estoque 7
Observação: anyMatch é uma operação terminal que retorna true assim que encontra o primeiro
elemento correspondendo ao predicado 6 , e interrompe a iteração (curto-circuito), tornando-a
eficiente – no exemplo, ao verificar "Smartphone", já pode retornar true sem verificar os demais
produtos.
Resposta 7.4:
Na lista employees , sabemos que Bob está inativo ( ativo=false ). Portanto, nem todos satisfazem
o predicado. allMatch retornará false assim que encontrar Bob 7 . A saída será:
Resposta 7.5:
27
Na nossa lista, as idades são 28,45,35,52,29 – nenhum é menor que 18. Portanto, o predicado
emp.idade < 18 nunca é verdadeiro para qualquer elemento. noneMatch retorna true nesse caso
(verdadeiro que nenhum elemento corresponde) 8 . A saída será:
Se houvesse pelo menos um funcionário menor de idade, noneMatch retornaria false. Em outras
palavras, noneMatch é equivalente à negação de anyMatch para o mesmo predicado: aqui ele está
confirmando que não existe funcionário que seja menor de 18.
Resposta 8.1:
Para a lista de exemplo: - Pedidos entregues têm clientes: "Maria" (pedido 1), "Maria" (pedido 3),
"Ana" (pedido 4). (Ignoramos "João" e "Pedro" pois seus pedidos estão pendentes.) - Distinct sobre
["Maria","Maria","Ana"] resulta em ["Maria","Ana"]. - Ordenando alfabeticamente: ["Ana","Maria"].
Saída:
[Ana, Maria]
Resposta 8.2:
No exemplo: - Pedidos com total > 500: pedido 3 (700.0, Maria), pedido 4 (1500.0, Ana). Pedido 2 (300) e
5 (130) não contam. - Somando 700.0 + 1500.0 = 2200.0.
Saída:
28
Total pedidos >500: 2200.0
Resposta 8.3:
Para a lista employees: - "IT": Alice, Carol (ambas ativas) -> ["Alice","Carol"] - "Sales": Eve (ativa; Bob é
inativo então filtrado) -> ["Eve"] - "HR": David (ativo) -> ["David"]
Saída:
Resposta 8.4:
Na lista exemplo: - Transações > 100: temos - "COMPRA": 1200.0 (sim), 800.0 (sim) -> 2 ocorrências -
"VENDA": 300.0 (sim), 2000.0 (sim) -> 2 ocorrências - "ESTORNO": 50.0 (não, filtrada fora) - Resultado:
{"COMPRA"=2, "VENDA"=2}. (Tipos com 0 não aparecem no mapa; "ESTORNO" ficou de fora.)
Saída possível:
29
COMPRA -> 2
VENDA -> 2
Resposta 8.5:
Saída:
Se não adicionássemos Server, seriam [Laptop, Smartphone, Monitor] (1200, 800, 300). Mas
considerando incluí-lo conforme discussão na análise, mantivemos 5 produtos com Server.
Resposta 8.6:
Combinação: filtrar entregues, flatMap itens, agrupar por categoria e somar quantidades:
Vamos testar mentalmente no exemplo: - Pedidos entregues: #1 e #3. - #1 itens: TV(cat "Eletrônicos", qt
1), Cabo HDMI("Acessórios", qt 2) - #3 itens: Mouse("Acessórios", qt 3), Teclado("Acessórios", qt 1) -
30
Agrupando: - "Eletrônicos" -> quantidades [1] - "Acessórios" -> quantidades [2, 3, 1] - Somando:
"Eletrônicos" = 1, "Acessórios" = 2+3+1 = 6.
Saída:
Eletrônicos -> 1
Acessórios -> 6
Isso indica, por exemplo, que ao todo foi vendido 1 item da categoria Eletrônicos e 6 itens de Acessórios
nos pedidos entregues.
Resposta 8.7:
Ativos: 4
Inativos: 1
Pois 4 dos 5 funcionários são ativos (Alice, Carol, David, Eve) e 1 (Bob) é inativo. O mapa
contagemAtivos seria {false=1, true=4} . Isso ilustra uso de partitioningBy + counting para
obter diretamente as contagens em vez das listas.
Resposta 8.8:
Realizando as operações:
31
Vamos calcular para [-2,-1,0,1,2,3] : - Quadrados: 4,1,0,1,4,9. - Distinct: [0,1,4,9] (ordem de
aparição no stream original, mas vamos ordenar em seguida de qualquer forma). - Sorted: [0,1,4,9]. -
UnmodifiableList: ainda [0,1,4,9], mas não mutável.
Saída impressa:
[0, 1, 4, 9]
Resposta 8.9:
Encadeando as operações:
Explicação: primeiro filtramos para remover "N/A" (e qualquer outra string não numérica). Em seguida
convertimos para int . Então usamos allMatch(idade >= 18) .
Saída:
Porque há pelo menos uma idade (17) menor que 18. Se substituíssemos "17" por "18" ou
removêssemos, então possivelmente seria true caso todos restantes >=18.
(Observação: também poderíamos ter usado noneMatch(idade < 18) no fim, que daria resultado
equivalente.)
32
Resposta 8.10:
Explicação: usamos mapToDouble para poder chamar .average() diretamente, que retorna um
OptionalDouble. Tratamos caso de não haver positivos.
Saída:
Média = 7.5
Caso a lista não tivesse nenhum número > 0, o OptionalDouble viria vazio e imprimiríamos a mensagem
de que não há valores positivos.
Resposta 9.1:
Primeiro, vamos implementar um Spliterator customizado para um intervalo de long. Esse Spliterator
vai iterar de start até end (inclusive talvez), e poderá se dividir aproximadamente ao meio.
Exemplo de implementação:
33
current++;
return true;
}
return false;
}
@Override
public Spliterator<Long> trySplit() {
long remaining = end - current + 1;
if(remaining < threshold) {
return null; // não divide se pouco restante
}
long mid = current + remaining/2; // ponto médio
Spliterator<Long> split = new RangeSpliterator(current, mid - 1);
current = mid;
return split;
}
@Override
public long estimateSize() {
return end - current + 1;
}
@Override
public int characteristics() {
return Spliterator.ORDERED | Spliterator.SIZED |
Spliterator.SUBSIZED | Spliterator.NONNULL | Spliterator.IMMUTABLE;
}
}
Comentários: - O Spliterator acima divide o intervalo em duas metades sempre que possível (enquanto
a metade tiver pelo menos threshold elementos, aqui 10000 arbitrariamente). - Características:
ORDERED (há uma ordem numérica), SIZED (podemos estimar tamanho), SUBSIZED (divisões também
sabem seu tamanho), NONNULL (não produz nulls), IMMUTABLE (os dados não mudam durante uso). -
tryAdvance fornece o próximo número e incrementa current .
34
Nesse código: - StreamSupport.stream(..., true) cria um stream paralelo a partir do nosso
Spliterator. - O ForkJoinPool.submit() garante que essa stream usará nosso pool em vez do
comum 9 10 . Chamamos .get() para aguardar o resultado. - Esperamos que a soma de 1 a
1_000_000 seja calculada (deveria ser 500000500000, fórmula da soma Gaussiana n*(n+1)/2). - Ao final,
fechamos o pool com shutdown() .
Esse exemplo mostra duas formas de tuning: 1. Spliterator customizado: para controlar como os
dados são divididos para paralelismo (por exemplo, evitar divisões ruins ou custos desbalanceados). 2.
Thread pool customizado: usando um pool próprio (4 threads) para executar a operação paralela, em
vez de usar o ForkJoinPool.commonPool() padrão que Streams paralelos usam por default 1 .
Ao executar o código acima, a saída deve confirmar a soma correta. Por exemplo, espera-se:
Resposta 9.2:
Vamos criar o Collector para lista imutável. Podemos fazê-lo manualmente ou usando Collector.of .
A implementação manual para entendimento:
35
}
}
Explicação: - supplier() fornece uma ArrayList vazia para começar a coleta. - accumulator()
adiciona um elemento na lista acumuladora. - combiner() combina duas listas (necessário em
paralelo) simplesmente adicionando a segunda na primeira. - finisher() transforma a lista mutável
resultante em uma lista imutável usando Collections.unmodifiableList . -
characteristics() retorna vazio: significa que precisamos aplicar o finisher (não é
IDENTITY_FINISH) e que não estamos marcando como CONCURRENT ou UNORDERED especificamente.
(Poderíamos marcar IDENTITY_FINISH se não estivéssemos transformando em unmodifiable, mas aqui
temos uma transformação final.)
Saída:
[A, B, C]
Vale notar que coletores customizados devem tomar cuidado com threads (por isso retornamos novas
listas e unimos corretamente) e com características (aqui não marcamos como UNORDERED ou
CONCURRENT, então o framework vai usar um único accumulator por thread em paralelo e combinar
no final com nosso combiner).
Resposta 9.3:
36
Uma forma de realizar a leitura resiliente é usar flatMap e tratar a exceção retornando um Stream
vazio quando falhar. Exemplo:
Explicação: - O flatMap para cada path tenta ler todas as linhas do arquivo ( Files.readAllLines
retorna List<String> com o conteúdo). - Em caso de sucesso, fazemos return linhas.stream()
para incorporar as linhas ao fluxo. - Em caso de falha (IOException, arquivo não encontrado, etc.),
logamos um erro (em stderr) e retornamos Stream.empty() . Isso efetivamente ignora aquele
arquivo no resultado agregado. - Continuamos o processamento com os demais arquivos.
Assim, se "inexistente.txt" gerar uma exceção, a mensagem de erro será impressa, mas o stream
continuará para "dados3.txt". O resultado todasLinhas conterá linhas de dados1.txt ,
dados2.txt , e dados3.txt (se existirem), mas nada de inexistente.txt .
Aqui 50 seria o total de linhas dos arquivos lidos com sucesso. O importante é que o pipeline não
lançou exceção e continuou processando outros elementos, graças ao tratamento interno.
Nota: Usamos Files.readAllLines que retorna uma lista, lendo o arquivo inteiro de uma vez.
Poderíamos usar Files.lines (que retorna Stream<String> lazy) dentro do flatMap, mas seria
necessário cuidar de fechá-lo. Alternativamente, poderíamos envolver Files.lines(path) em um
try-with-resources dentro do lambda: ler as linhas, coletar numa lista e depois fazer stream dessa lista.
Para simplificar, readAllLines foi adequado.
Resposta 9.4:
37
List<Integer> resultado = lista.stream()
.peek(x -> System.out.println("Original: " + x))
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("Filtrado (par): " + x))
.map(x -> x * x)
.peek(x -> System.out.println("Mapeado (quadrado): " + x))
.collect(Collectors.toList());
System.out.println("Resultado final: " + resultado);
Original: 1
Original: 2
Filtrado (par): 2
Mapeado (quadrado): 4
Original: 3
Original: 4
Filtrado (par): 4
Mapeado (quadrado): 16
Resultado final: [4, 16]
Notavelmente, a saída não está agrupada em blocos por estágio (não vemos todos "Original" primeiro,
depois todos "Filtrado"). Ao invés disso, vemos que o stream processa elemento por elemento,
passando por todas etapas antes de ir para o próximo elemento. Isso acontece porque as operações
intermediárias do Stream são lazy e compõem um pipeline fusionado: o elemento 1 é processado até
ser descartado pelo filtro; em seguida o elemento 2 é processado completamente (até map) antes de
consumir o 3, e assim por diante, em uma execução loteada elemento a elemento.
Esse intercalamento fica evidente com os logs do peek, confirmando a natureza de avaliação lazy e per-
element do pipeline de streams. O resultado final, coletado e impresso, é [4, 16] correspondendo
aos quadrados dos números pares da lista original.
Resposta 9.5:
Vamos montar um exemplo comparativo. Criaremos, por exemplo, um array de 10 milhões de inteiros e
somá-los de três formas. Para evitar influências de warm-up, poderíamos repetir cada medição algumas
vezes, mas aqui faremos uma medição básica. Código de exemplo:
38
// Preparar dados
int N = 10_000_000;
int[] array = new int[N];
for(int i=0; i<N; i++) {
array[i] = i; // por simplicidade, 0,1,2,... (soma conhecida)
}
// 1. Loop imperativo
long t0 = System.nanoTime();
long soma1 = 0;
for(int x : array) {
soma1 += x;
}
long t1 = System.nanoTime();
// 2. Stream sequencial
long soma2 = Arrays.stream(array).asLongStream().sum(); // sum() terminal
long t2 = System.nanoTime();
// 3. Stream paralelo
long soma3 = Arrays.stream(array).parallel().asLongStream().sum();
long t3 = System.nanoTime();
• O loop for direto foi bastante rápido (~30 ms). Isso se deve a ser bem otimizado e sem overhead
de abstração.
• O Stream sequencial foi mais lento (~65 ms), mais que o dobro do tempo do loop no teste. Isso
ocorre porque há overhead na criação do stream, boxe/desboxe (embora usamos asLongStream
para evitar boxing de Integer), e iteração interna genérica. Em operações simples, esse overhead
pesa mais do que o loop manual.
39
• O Stream paralelo teve desempenho similar ou ligeiramente melhor que o loop (~29 ms,
marginalmente menor que 30 ms do loop). Em alguns runs, paralelo poderia ser até um pouco
mais rápido se os dados forem grandes e CPU tiver núcleos ociosos. Em nossa medição, o ganho
não foi grande porque somar números é muito rápido, então a sobrecarga de paralelismo quase
anula o benefício. Porém, notamos que ele conseguiu equiparar o tempo do loop e foi cerca de
2x mais rápido que o stream sequencial graças ao uso de 4 threads.
Esses resultados ilustram que: - Para tarefas simples (soma de números, filtragem leve) e tamanhos
moderados, o loop tradicional costuma ser mais rápido que streams 3 , por eliminar overhead.
Streams sequenciais adicionam camadas de abstração que custam alguns nanos por elemento. -
Streams paralelos podem superar loops e streams sequenciais quando há bastante trabalho a ser
dividido e a máquina possui múltiplos núcleos. No exemplo, para 10 milhões de operações muito
simples, o ganho de paralelismo foi modesto. Se a operação fosse mais complexa (CPU-bound), o
parallelStream tenderia a mostrar melhoria maior proporcionalmente. - Importante: Paralelismo
envolve custos de gerenciamento de threads, divisão de tarefas e combinação de resultados. Portanto,
só compensa para volumes grandes de dados ou operações custosas. Caso contrário, pode até
degradar a performance.
Em resumo, o loop imperativo simples permanece muito eficiente para operações simples em coleções
pequenas/médias. A Stream API traz vantagens de código mais conciso e expressivo, e brilha
especialmente quando usamos pipelines complexos e paralelismo em dados grandes. Entretanto, há
um custo inerente a essa abstração. Conforme observado, no exemplo benchmark, o loop foi ~2x mais
rápido que o stream sequencial 11 . Com parallelStream, conseguimos recuperar performance usando
múltiplos cores, chegando perto ou ultrapassando o loop, mas isso depende do hardware e do caso de
uso.
Citando de forma geral: "for-loops performed much better than streams from the performance
perspective" 3 no cenário apresentado, embora "isso pode mudar em alguns casos, especialmente com
streams paralelos" 12 . Portanto, ao escolher entre estilo imperativo e Streams, deve-se considerar o
tamanho dos dados, complexidade das operações e necessidade de paralelismo.
40