Anotação de tradução: mapear (mapear) e reduzir (reduzir, simplificar) são dois conceitos muito básicos em matemática. Eles apareceram em várias linguagens de programação funcional por muito tempo. Somente em 2003 o Google os levou adiante. aplicou-os a Depois que a computação paralela foi implementada em sistemas distribuídos, o nome dessa combinação começou a brilhar no mundo da informática (aqueles fãs funcionais podem não pensar assim). Neste artigo, veremos a estreia das combinações de mapa e redução após o Java 8 suportar programação funcional (esta é apenas uma introdução preliminar, haverá tópicos especiais sobre eles posteriormente).
Reduzir um conjunto
Até agora introduzimos diversas técnicas novas para operar coleções: encontrar elementos correspondentes, encontrar elementos individuais e transformações de coleções. Essas operações têm uma coisa em comum: todas operam em um único elemento da coleção. Não há necessidade de comparar elementos ou realizar operações em dois elementos. Nesta seção, veremos como comparar elementos e manter dinamicamente o resultado de uma operação durante o percurso da coleção.
Vamos começar com exemplos simples e depois progredir. No primeiro exemplo, primeiro percorremos a coleção de amigos e calculamos o número total de caracteres em todos os nomes.
Copie o código do código da seguinte forma:
System.out.println("Número total de caracteres em todos os nomes: " + friends.stream()
.mapToInt(nome -> nome.comprimento())
.soma());
Para calcular o número total de caracteres precisamos saber o comprimento de cada nome. Isso pode ser feito facilmente através do método mapToInt(). Depois de convertermos os nomes em comprimentos correspondentes, só precisamos adicioná-los no final. Temos um método sum() integrado para fazer isso. Aqui está o resultado final:
Copie o código do código da seguinte forma:
Número total de caracteres em todos os nomes: 26
Usamos uma variante da operação do mapa, o método mapToInt() (como mapToInt, mapToDouble, etc., que irá gerar tipos específicos de fluxos, como IntStream, DoubleStream), e então calculamos o número total de caracteres com base no comprimento retornado.
Além de usar o método sum, existem muitos métodos semelhantes que podem ser usados, como max() para encontrar o comprimento máximo, min() para encontrar o comprimento mínimo, sorted() para classificar os comprimentos, Average() para encontre o comprimento médio, etc.
Outro aspecto atraente do exemplo acima é o modo MapReduce cada vez mais popular. O método map() executa o mapeamento, e o método sum() é uma operação de redução comumente usada. Na verdade, a implementação do método sum() no JDK usa o método reduz(). Vamos dar uma olhada em algumas das formas mais comumente usadas de operações de redução.
Por exemplo, iteramos todos os nomes e imprimimos aquele com o nome mais longo. Se houver vários nomes mais longos, imprimimos o encontrado primeiro. Uma maneira é calcularmos o comprimento máximo e então selecionar o primeiro elemento que corresponda a esse comprimento. No entanto, fazer isso requer percorrer a lista duas vezes - o que é muito ineficiente. É aqui que a operação de redução entra em ação.
Podemos usar a operação de redução para comparar os comprimentos de dois elementos, depois retornar o mais longo e compará-lo ainda mais com os elementos restantes. Como outras funções de ordem superior que vimos antes, o método reduz() também percorre toda a coleção. Entre outras coisas, registra o resultado do cálculo retornado pela expressão lambda. Se houver um exemplo que possa nos ajudar a entender isso melhor, vamos primeiro examinar um trecho de código.
Copie o código do código da seguinte forma:
final Opcional<String> aLongName = friends.stream()
.reduce((nome1, nome2) ->
nome1.comprimento() >= nome2.comprimento() ? nome1: nome2);
aLongName.ifPresent(nome ->
System.out.println(String.format("Um nome mais longo: %s", nome)));
A expressão lambda passada para o método reduz() recebe dois parâmetros, nome1 e nome2, e compara seus comprimentos e retorna o mais longo. O método reduzir() não tem ideia do que vamos fazer. Essa lógica é eliminada na expressão lambda que passamos - esta é uma implementação leve do padrão Strategy.
Esta expressão lambda pode ser adaptada ao método apply da interface funcional de um BinaryOperator no JDK. Este é exatamente o tipo de argumento que o método reduzir aceita. Vamos executar este método de redução e ver se ele consegue selecionar corretamente o primeiro dos dois nomes mais longos.
Copie o código do código da seguinte forma:
Um nome mais longo: Brian
Quando o método reduzir() percorre a coleção, ele primeiro chama a expressão lambda nos dois primeiros elementos da coleção e o resultado retornado pela chamada continua a ser usado na próxima chamada. Na segunda chamada, o valor de name1 está vinculado ao resultado da chamada anterior e o valor de name2 é o terceiro elemento da coleção. Os elementos restantes também são chamados nesta ordem. O resultado da última chamada de expressão lambda é o resultado retornado por todo o método reduzir().
O método reduzir() retorna um valor Opcional porque a coleção passada para ele pode estar vazia. Nesse caso, não haveria nome mais longo. Se a lista tiver apenas um elemento, o método reduzir retorna diretamente esse elemento e não chama a expressão lambda.
A partir deste exemplo podemos inferir que o resultado da redução só pode ser no máximo um elemento do conjunto. Se desejarmos retornar um valor padrão ou base, podemos usar uma variante do método reduz() que aceita um parâmetro adicional. Por exemplo, se o nome mais curto for Steve, podemos passá-lo para o método reduz() assim:
Copie o código do código da seguinte forma:
String final steveOrLonger = friends.stream()
.reduce("Steve", (nome1, nome2) ->
nome1.comprimento() >= nome2.comprimento() ? nome1: nome2);
Se houver um nome maior que ele, então esse nome será selecionado; caso contrário, o valor base Steve será retornado. Esta versão do método reduz() não retorna um objeto Opcional, pois se a coleção estiver vazia, um valor padrão será retornado independentemente do caso em que não haja valor de retorno;
Antes de terminarmos este capítulo, vamos dar uma olhada em uma operação muito básica, mas não tão fácil, em operações com conjuntos: mesclar elementos.
Mesclar elementos
Aprendemos como encontrar elementos, percorrer e converter coleções. No entanto, há outra operação comum - unir elementos de coleção - sem esta função join() recém-adicionada, o código conciso e elegante mencionado anteriormente seria em vão. Este método simples é tão prático que se tornou uma das funções mais utilizadas no JDK. Vamos ver como usá-lo para imprimir elementos de uma lista, separados por vírgulas.
Ainda usamos essa lista de amigos. Se você usar o método antigo da biblioteca JDK, o que deverá fazer se quiser imprimir todos os nomes separados por vírgulas?
Temos que percorrer a lista e imprimir os elementos um por um. O loop for em Java 5 foi melhorado em relação ao anterior, então vamos usá-lo.
Copie o código do código da seguinte forma:
for(String nome: amigos) {
System.out.print(nome + ", ");
}
System.out.println();
O código é muito simples, vamos ver qual é o seu resultado.
Copie o código do código da seguinte forma:
Brian, Nate, Neal, Raju, Sara, Scott,
Droga, tem aquela vírgula chata no final (podemos culpar aquele Scott no final?). Como posso dizer ao Java para não colocar vírgula aqui? Infelizmente, o loop é executado passo a passo e não é fácil fazer algo especial no final. Para resolver este problema, podemos usar o método do loop original.
Copie o código do código da seguinte forma:
for(int i = 0; i < amigos.size() - 1; i++) {
System.out.print(amigos.get(i) + ", ");
}
if(amigos.tamanho() > 0)
System.out.println(amigos.get(amigos.size() - 1));
Vamos ver se o resultado desta versão está OK.
Copie o código do código da seguinte forma:
Brian, Nate, Neal, Raju, Sara, Scott
O resultado ainda é bom, mas este código não é lisonjeiro. Salve-nos, Java.
Não precisamos mais suportar essa dor. A classe StringJoiner em Java 8 nos ajuda a resolver esses problemas. Além disso, a classe String também adiciona um método join para que possamos substituir o material acima por uma linha de código.
Copie o código do código da seguinte forma:
System.out.println(String.join(", ", amigos));
Venha dar uma olhada, os resultados são tão satisfatórios quanto o código.
Copie o código do código da seguinte forma:
Brian, Nate, Neal, Raju, Sara, Scott
O resultado ainda é bom, mas este código não é lisonjeiro. Salve-nos, Java.
Não precisamos mais suportar essa dor. A classe StringJoiner em Java 8 nos ajuda a resolver esses problemas. Além disso, a classe String também adiciona um método join para que possamos substituir o material acima por uma linha de código.
Copie o código do código da seguinte forma:
System.out.println(String.join(", ", amigos));
Venha dar uma olhada, os resultados são tão satisfatórios quanto o código.
Copie o código do código da seguinte forma:
Brian, Nate, Neal, Raju, Sara, Scott
Na implementação subjacente, o método String.join() chama a classe StringJoiner para dividir o valor passado como o segundo parâmetro (que é um parâmetro de comprimento variável) em uma string longa, usando o primeiro parâmetro como delimitador. Claro, esse método é mais do que apenas juntar vírgulas. Por exemplo, podemos passar vários caminhos e facilmente definir um caminho de classe, graças a esses métodos e classes recém-adicionados.
Já sabemos como conectar elementos de listas. Antes de conectar listas, também podemos transformar os elementos. Claro, também sabemos como usar o método map para transformar listas. A seguir, também podemos usar o método filter() para filtrar os elementos que desejamos. O último passo para conectar os elementos da lista, usando vírgulas ou algum outro delimitador, é apenas uma simples operação de redução.
Podemos usar o método reduzir() para concatenar os elementos em uma string, mas isso requer algum trabalho de nossa parte. JDK tem um método collect() muito conveniente, que também é uma variante de reduzir(). Podemos usá-lo para combinar elementos em um valor desejado.
O método collect() executa a operação de redução, mas delega a operação específica a um coletor para execução. Podemos mesclar os elementos convertidos em um ArrayList. Continuando com o exemplo anterior, podemos concatenar os elementos convertidos em uma string separada por vírgula.
Copie o código do código da seguinte forma:
Sistema.out.println(
amigos.stream()
.map(String::toUpperCase)
.collect(junção(", ")));
Chamamos o método collect() na lista convertida, passando-o por um coletor retornado pelo método join() Joining é um método estático na classe de ferramenta Collectors. O coletor é como um receptor. Ele recebe os objetos passados pelo coletor e os armazena no formato desejado: ArrayList, String, etc. Exploraremos esse método mais detalhadamente no método collect e na classe Collectors na página 52.
Este é o nome da saída, agora estão em letras maiúsculas e separados por vírgulas.
Copie o código do código da seguinte forma:
BRIAN, NATE, NEAL, RAJU, SARA, SCOTT
Resumir
As coleções são muito comuns na programação. Com as expressões lambda, as operações de coleta do Java tornaram-se mais simples e fáceis. Todo o código antigo e desajeitado para operações de coleta pode ser substituído por esta nova abordagem elegante e concisa. O iterador interno torna a travessia e transformação da coleção mais conveniente, longe do problema da variabilidade, e encontrar elementos da coleção torna-se extremamente fácil. Você pode escrever muito menos código usando esses novos métodos. Isso torna o código mais fácil de manter, mais focado na lógica de negócios e menos operações básicas de programação.
No próximo capítulo veremos como as expressões lambda simplificam outra operação básica no desenvolvimento de programas: manipulação de strings e comparação de objetos.