Introdução ao uso de streams:
Novos usuários podem achar difícil expressar cálculos em pipelines de stream.
Streams são baseadas em programação funcional, oferecendo expressividade, rapidez e paralelização.
Estruturação do cálculo:
Estruturar cálculos como sequências de transformações usando funções puras.
Funções puras dependem apenas de suas entradas e não alteram estado.
Efeitos colaterais:
Evitar efeitos colaterais em funções passadas para operações de stream.
Uso inadequado de forEach que altera estado externo é um “bad smell”.
Exemplo 1: Código com efeitos colaterais
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
Problema: Esse código usa forEach para modificar o estado externo (freq). Ele é iterativo e não aproveita as vantagens das streams.
Exemplo 2: Código sem efeitos colaterais
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(Collectors.groupingBy(String::toLowerCase, Collectors.counting()));
}
Solução: Utiliza o coletor Collectors.groupingBy para criar a tabela de frequência sem alterar o estado externo. Mais curto, claro e eficiente.
Apropriação da API de streams:
O código que imita loops iterativos não tira vantagem das streams.
Utilizar coletores (Collector) para operações mais eficientes e legíveis.
Coletores:
Simplificam a coleta de resultados em coleções como listas e conjuntos.
Collectors.toList(), Collectors.toSet(), Collectors.toCollection(collectionFactory).
Exemplo 3: Extraindo uma lista das dez palavras mais frequentes
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(10)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
Explicação:
Ordena as entradas do mapa de frequência em ordem decrescente de valor.
Limita a stream a 10 palavras.
Coleta as palavras mais frequentes em uma lista.
Complexidade da API Collectors:
API possui 39 métodos, mas muitos são para uso avançado.
Coletores podem ser usados para criar mapas (toMap, groupingBy).
Mapas e estratégias de coleta:
toMap(keyMapper, valueMapper) para chave-valor únicos.
Estratégias para lidar com conflitos de chaves usando função merge.
groupingBy para agrupar elementos em categorias baseadas em funções classificadoras.
Exemplo 4: Usando toMap com função merge
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(Collectors.toMap(
String::toLowerCase,
word -> 1L,
Long::sum
));
}
Explicação:
toMap mapeia palavras para suas frequências.
Função merge (Long::sum) lida com conflitos de chave, somando as frequências.
Exemplo 5: Agrupando álbuns por artista e encontrando o álbum mais vendido
.collect(Collectors.toMap(
Album::getArtist,
Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(Album::sales))
));
Explicação:
toMap mapeia artistas para seus álbuns mais vendidos.
BinaryOperator.maxBy determina o álbum mais vendido para cada artista.
Coleta de strings:
Collectors.joining para concatenar strings com delimitadores opcionais.
Exemplo 6: Concatenando strings com delimitador
.collect(Collectors.joining(“, “, “[“, “]”));
Explicação:
Collectors.joining concatena strings com uma vírgula como delimitador, prefixo e sufixo.
Resultado: [came, saw, conquered].
Conclusão:
Essência das streams está em funções sem efeitos colaterais.
forEach deve ser usado apenas para reportar resultados.
Conhecimento sobre coletores é essencial para uso eficaz das streams.