Usando escopo léxico e fechamentos
Muitos desenvolvedores têm esse mal-entendido, acreditando que o uso de expressões lambda levará à redundância de código e reduzirá a qualidade do código. Pelo contrário, por mais complexo que o código se torne, não comprometeremos a qualidade do código em prol da simplicidade, como veremos a seguir.
Conseguimos reutilizar a expressão lambda no exemplo anterior, porém, se combinarmos outra letra, o problema de redundância de código retorna rapidamente; Vamos analisar esse problema primeiro e, em seguida, usar escopo léxico e fechamentos para resolvê-lo.
Redundância causada por expressões lambda
Vamos filtrar as letras que começam com N ou B de amigos. Continuando com o exemplo acima, o código que escrevemos pode ser assim:
Copie o código do código da seguinte forma:
final Predicate<String>startWithN = nome -> name.startsWith("N");
final Predicate<String>startWithB = nome -> name.startsWith("B");
contagem longa finalFriendsStartN =
amigos.stream()
.filter(iniciaComN).count();
contagem longa finalFriendsStartB =
amigos.stream()
.filter(iniciaComB).count();
O primeiro predicado determina se o nome começa com N, e o segundo determina se o nome começa com B. Passamos essas duas instâncias para duas chamadas de método de filtro, respectivamente. Isso parece razoável, mas os dois predicados são redundantes, são apenas letras diferentes no cheque. Vamos ver como podemos evitar essa redundância.
Use escopo léxico para evitar redundância
Na primeira solução, podemos extrair as letras como parâmetros da função e passar esta função para o método filter. Este é um bom método, mas o filtro não é aceito por todas as funções. Ele aceita apenas funções com apenas um parâmetro. Esse parâmetro corresponde ao elemento da coleção e retorna um valor booleano. Ele espera que o que é passado seja um Predicado.
Esperamos que haja um local onde esta carta possa ser armazenada em cache até que o parâmetro seja passado (neste caso, o parâmetro name). Vamos criar uma nova função como esta.
Copie o código do código da seguinte forma:
public static Predicate<String> checkIfStartsWith(final String letra) {
retornar nome -> nome.startsWith(letra);
}
Definimos uma função estática checkIfStartsWith, que recebe um parâmetro String e retorna um objeto Predicate, que pode ser passado ao método filter para uso posterior. Ao contrário das funções de ordem superior que vimos anteriormente, que utilizam funções como parâmetros, este método retorna uma função. Mas também é uma função de ordem superior, que já mencionamos em Evolução, não mudança, na página 12.
O objeto Predicate retornado pelo método checkIfStartsWith é um pouco diferente de outras expressões lambda. Na instrução return name -> name.startsWith(letter), sabemos exatamente o que é name, é o parâmetro passado para a expressão lambda. Mas o que exatamente é a letra variável? Está fora do domínio da função anônima Java encontra o domínio onde a expressão lambda está definida e descobre a letra da variável. Isso é chamado de escopo lexical. O escopo léxico é algo muito útil, pois nos permite armazenar em cache uma variável em um escopo para uso posterior em outro contexto. Como essa expressão lambda utiliza variáveis em seu escopo, essa situação também é chamada de encerramento. Em relação às restrições de acesso ao escopo lexical, você pode ler as restrições ao escopo lexical na página 31?
Há alguma restrição no escopo lexical?
Em uma expressão lambda, só podemos acessar os tipos finais em seu escopo ou mesmo variáveis locais do tipo final.
A expressão lambda pode ser chamada imediatamente, atrasada ou de um thread diferente. Para evitar conflitos de corrida, as variáveis locais no domínio que acessamos não podem ser modificadas depois de inicializadas. Qualquer operação de modificação causará exceção de compilação.
Marcar como final resolve esse problema, mas Java não nos obriga a marcá-lo dessa forma. Na verdade, Java analisa duas coisas. Uma é que a variável acessada deve ser inicializada no método em que está definida e antes da expressão lambda ser definida. Segundo, os valores dessas variáveis não podem ser modificados – ou seja, elas são de fato do tipo final, embora não sejam marcadas como tal.
Expressões lambda sem estado são constantes de tempo de execução, enquanto aquelas que usam variáveis locais têm sobrecarga computacional adicional.
Ao chamar o método filter, podemos usar a expressão lambda retornada pelo método checkIfStartsWith, assim:
Copie o código do código da seguinte forma:
contagem longa finalFriendsStartN =
amigos.stream() .filter(checkIfStartsWith("N")).count();
contagem longa finalFriendsStartB = friends.stream()
.filter(checkIfStartsWith("B")).count();
Antes de chamar o método filter, primeiro chamamos o método checkIfStartsWith() e passamos as letras desejadas. Essa chamada retorna rapidamente uma expressão lambda, que então passamos para o método filter.
Ao criar uma função de ordem superior (checkIfStartsWith neste caso) e usar o escopo léxico, removemos com sucesso a redundância do código. Não precisamos mais determinar repetidamente se o nome começa com uma determinada letra.
Refatore, reduza o escopo
No exemplo anterior usamos um método estático, mas não queremos usar o método estático para armazenar variáveis em cache, o que bagunçaria nosso código. É melhor restringir o escopo desta função ao local onde ela é usada. Podemos usar uma interface Function para conseguir isso.
Copie o código do código da seguinte forma:
final Function<String, Predicate<String>>startWithLetter = (String letra) -> {
Predicado<String> checkStarts = (String nome) -> name.startsWith(letra);
return checkStarts;
Esta expressão lambda substitui o método estático original. Ela pode ser colocada em uma função e definida antes de ser necessária. A variável startWithLetter refere-se a uma Função cujo parâmetro de entrada é String e cujo parâmetro de saída é Predicate.
Em comparação com o método estático, esta versão é muito mais simples, mas podemos continuar a refatorá-la para torná-la mais concisa. Do ponto de vista prático, esta função é igual ao método estático anterior, ambos recebem uma String e retornam um Predicado; Em vez de declarar explicitamente um Predicado, substituímo-lo inteiramente por uma expressão lambda.
Copie o código do código da seguinte forma:
função final<String, Predicate<String>>startWithLetter = (String letter) -> (String name) -> name.startsWith(letter);
Acabamos com a bagunça, mas também podemos remover a declaração de tipo para torná-la mais concisa, e o compilador Java fará a dedução de tipo com base no contexto. Vamos dar uma olhada na versão melhorada.
Copie o código do código da seguinte forma:
Função final<String, Predicate<String>>startWithLetter =
letra -> nome -> nome.startsWith(letra);
É preciso algum esforço para se adaptar a esta sintaxe concisa. Se isso cegar você, procure primeiro outro lugar. Concluímos a refatoração do código e agora podemos usá-lo para substituir o método checkIfStartsWith() original, assim:
Copie o código do código da seguinte forma:
contagem longa finalFriendsStartN = friends.stream()
.filter(startsWithLetter.apply("N")).count();
contagem longa finalFriendsStartB = friends.stream()
.filter(startsWithLetter.apply("B")).count();
Nesta seção, usamos funções de ordem superior. Vimos como criar funções dentro de funções se passarmos uma função para outra função e como retornar uma função de uma função. Todos esses exemplos demonstram a simplicidade e a reutilização trazidas pelas expressões lambda.
Nesta seção utilizamos totalmente as funções de Função e Predicado, mas vamos dar uma olhada na diferença entre elas. Predicado aceita um parâmetro do tipo T e retorna um valor booleano para representar o verdadeiro ou falso de sua condição de julgamento correspondente. Quando precisarmos fazer julgamentos condicionais, podemos usar Predicateg para completá-lo. Métodos como filter que filtram elementos recebem Predicate como parâmetro. Funciton representa uma função cujos parâmetros de entrada são variáveis do tipo T e retorna um resultado do tipo R. É mais geral que Predicate, que só pode retornar booleano. Contanto que a entrada seja convertida em uma saída, podemos usar Function, portanto, é razoável que o mapa use Function como parâmetro.
Como você pode ver, selecionar elementos de uma coleção é muito simples. A seguir apresentaremos como selecionar apenas um elemento da coleção.