Item 46: Dê preferência às funções sem efeitos colaterais nas streams

RMAG news

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

Map<String, Long> freq = new HashMap<>();
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

Map<String, Long> freq;
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

List<String> topTen = freq.entrySet().stream()
.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

Map<String, Long> freq;
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

Map<Artist, Album> topAlbums = albums.stream()
.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

String result = Stream.of(“came”, “saw”, “conquered”)
.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.

Please follow and like us:
Pin Share