Aprenda nesse artigo a utilizar as funções lambda em Java, conceito esse adicionado ao Java 8, e que tem como principal objetivo adicionar ao Java técnicas de linguagens funcionais, como Scala e LISP. A grande vantagem de funções lambda é diminuir a quantidade de código necessária para a escrita de algumas funções, como por exemplo, as classes internas que são necessárias em diversas partes da linguagem Java, como Listeners e Threads.
Simplificando um pouco a definição, uma função lambda é uma função sem declaração, isto é, não é necessário colocar um nome, um tipo de retorno e o modificador de acesso. A ideia é que o método seja declarado no mesmo lugar em que será usado. As funções lambda em Java tem a sintaxe definida como (argumento) -> (corpo), como mostram alguns exemplos da Listagem 1.
(int a, int b) -> { return a + b; }
() -> System.out.println("Hello World");
(String s) -> { System.out.println(s); }
() -> 42
() -> { return 3.1415 };
a -> a > 10
Uma função lambda pode ter nenhum ou vários parâmetros e seus tipos podem ser colocados ou podem ser omitidos, dessa forma, eles são inferidos pelo Java (veremos alguns exemplos disso mais para frente). A função lambda pode ter nenhum ou vários comandos: se a mesma tiver apenas um comando as chaves, não são obrigatórias e a função retorna o valor calculado na expressão; se a função tiver vários comandos, é necessário colocar as chaves e também o comando return - se nada for retornado, a função tem um retorno void.
Para demonstrar como utilizar as funções lambada em Java, vamos analisar alguns casos. O primeiro exemplo será a utilização com threads, onde elas são muito utilizadas para simplificar o código. O segundo exemplo será a utilização com as classes de coleção do Java, para aumentar a flexibilidade de diversas funções, como ordenar e filtrar listas. O terceiro exemplo será a utilização com classes do tipo Listeners, onde as funções lambda são utilizadas para simplificar o código. E finalmente, o quarto exemplo será para a criação de funções genéricas, que aceitam expressões lambda como parâmetros, e podem ser utilizadas em diversos problemas.
Exemplo 1 – Funções Lambda com Threads
Para exemplificar a utilização de expressões lambda com threads será analisado um programa que cria uma thread com uma função interna e que vai apenas mostrar a mensagem “Thread com classe interna!”. A Listagem 2 mostra o código dessa implementação.
Runnable r = new Runnable() {
public void run() {
System.out.println("Thread com classer interna!");
}
};
new Thread(r).start();
Primeiro é criada uma implementação do método run da interface Runnable, e em seguida é criada a Thread com essa implementação. É possível verificar a grande quantidade de código necessário para um exemplo bastante simples.
Já com a utilização de expressões lambda, o código necessário para a implementação dessa mesma funcionalidade é bastante simples e bem menor que o anterior. A Listagem 3 mostra um exemplo do código da expressão lambda com threads.
Runnable r = () -> System.out.println("Thread com função lambda!");
new Thread(r).start();
Essa expressão não passa nenhum parâmetro, pois ela será passada para a função run, definida na interface Runnable, que não tem nenhum parâmetro, então ela também não tem nenhum retorno.
Um código ainda mais simples é a passagem da função diretamente como parâmetro para o construtor da classe Thread. A Listagem 4 mostra um exemplo desse código, mostrando que as funções lambda podem ser definidas e passadas como parâmetros diretamente para outros métodos, e isso pode ser bastante útil, como veremos nos próximos exemplos.
new Thread(
() -> System.out.println("hello world")
).start();
Exemplo 2 – Funções Lambda com as classes de Collections
As funções lambdas podem ser bastante utilizadas com as classes de coleções do Java, pois nessas fazemos diversos tipos de funções que consistem basicamente em percorrer a coleção e fazer uma determinada ação, como por exemplo, imprimir todos os elementos da coleção, filtrar elementos da lista e buscar um determinado valor na lista.
A Listagem 5 mostra um exemplo de como normalmente é feito o código para percorrer uma lista e imprimir os valores dentro dela.
System.out.println("Imprime todos os elementos da lista!");
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
for(Integer n: list) {
System.out.println(n);
}
Com as funções lambda é possível implementar a mesma funcionalidade com muito menos código, bastando chamar o método forEach de uma lista, que é um método que espera uma função lambda como parâmetro. Esse método executará, a cada iteração na lista, a função passada como parâmetro. A Listagem 6 mostra o exemplo de imprimir todos os elementos de uma lista com expressão lambda.
System.out.println("Imprime todos os elementos da lista!");
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
list.forEach(n -> System.out.println(n));
Dentro do código de uma função lambda é possível executar diversos comandos, como por exemplo, na Listagem 7, que antes de imprimir o número, verifica se ele é par ou ímpar: se for par o numero é impresso, caso contrário, nada é realizado. Nesse exemplo é possível verificar que dentro de uma expressão lambda pode ser realizado qualquer tipo de operação.
System.out.println("Imprime todos os elementos pares da lista!");
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
list.forEach(n -> {
if (n % 2 == 0) {
System.out.println(n);
}
});
Mais um exemplo do que pode ser feito com funções lambda, são expressões matemáticas. Na Listagem 8 é exibido o código para mostrar o quadrado de todos os elementos de uma lista de números inteiros.
System.out.println("Imprime o quadrado de todos os elementos da lista!");
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
list.forEach(n -> System.out.println(n * n));
Funções lambda podem ser utilizadas também para a ordenação de listas com a interface Comparator. Por exemplo, caso exista uma classe Pessoa com os atributos nome e idade e é necessário ordenar uma lista em ordem alfabética pelo nome, ou em ordem das idades, é necessário implementar dois comparators, um para cada tipo de parâmetro, e chamá-lo no método sort da lista que será ordenada. A Listagem 9 mostra o código do exemplo descrito utilizando a interface Comparator tradicional. O código da classe Pessoa foi omitido, mas ela é uma classe bastante simples, apenas com os atributos nome e idade, o construtor e os métodos get e set.
System.out.println("Ordenando pessoas pelo nome:");
List<Pessoa> listPessoas = Arrays.asList(new Pessoa("Eduardo", 29),
new Pessoa("Luiz", 32), new Pessoa("Bruna", 26));
Collections.sort(listPessoas, new Comparator<Pessoa>() {
@Override
public int compare(Pessoa pessoa1, Pessoa pessoa2){
return pessoa1.getNome().compareTo(pessoa2.getNome());
}
});
listPessoas.forEach(p -> System.out.println(p.getNome()));
System.out.println("Ordenando pessoas pela idade:");
Collections.sort(listPessoas, new Comparator<Pessoa>() {
@Override
public int compare(Pessoa pessoa1, Pessoa pessoa2){
return pessoa1.getIdade().compareTo(pessoa2.getIdade());
}
});
listPessoas.forEach(p -> System.out.println(p.getNome()));
É fácil observar que o código, apesar de bastante simples, ficou muito grande, apenas para ordenar duas vezes a lista com dois parâmetros diferentes.
É possível reimplementar esse exemplo utilizando funções lambda e deixando o código muito mais conciso. A Listagem 10 mostra a reimplementarão da Listagem 9, mas agora utilizando expressões lambda.
List<Pessoa> listPessoas = Arrays.asList(new Pessoa("Eduardo", 29),
new Pessoa("Luiz", 32), new Pessoa("Bruna", 26));
System.out.println("Ordenando pessoas pelo nome:");
Collections.sort(listPessoas, (Pessoa pessoa1, Pessoa pessoa2)
-> pessoa1.getNome().compareTo(pessoa2.getNome()));
listPessoas.forEach(p -> System.out.println(p.getNome()));
System.out.println("Ordenando pessoas pela idade:");
Collections.sort(listPessoas, (Pessoa pessoa1, Pessoa pessoa2)
-> pessoa1.getIdade().compareTo(pessoa2.getIdade()));
listPessoas.forEach(p -> System.out.println(p.getNome()));
Vejam que os dois exemplos implementam exatamente a mesma funcionalidade, mas o código utilizando expressões lambda tem apenas cinco linhas, enquanto que o código que não utiliza expressões lambda tem 15 linhas.
Com expressões lambda também é possível filtrar elementos de uma coleção de objetos criando para isso um stream de dados (também um novo conceito do Java 8). É chamado o método filter do stream e como parâmetro para esse método é passado uma função lambda que faz o filtro dos elementos desejados. A Listagem 11 mostra dois exemplos de filtros sendo realizados também na listagem de pessoas utilizadas nos exemplos anteriores. O primeiro filtro é feito apenas para pessoas com mais de 30 anos e o segundo para apenas pessoas que tem o nome iniciado com a letra “E”.
System.out.println("Pessoas com mais de 30 anos:");
List<Pessoa> maioresTrinta = listPessoas.stream().filter(p
-> p.getIdade() > 30).collect(Collectors.toList());
maioresTrinta.forEach(p -> System.out.println(p.getNome()));
System.out.println("Pessoas que o nome iniciam com E:");
List<Pessoa> nomesIniciadosE = listPessoas.stream().filter(p
-> p.getNome().startsWith("E")).collect(Collectors.toList());
nomesIniciadosE.forEach(p -> System.out.println(p.getNome()));
Exemplo 3 – Funções Lambda utilizadas com Listeners
Listeners são classes que implementam o padrão de projeto Observer, que representa objetos que ficam esperando ações realizadas em outros objetos e, a partir dessa ação, executam algum código. Um exemplo bem comum são os Listeners de botões da API de interfaces gráficas Swing. Por exemplo, quando é necessário implementar um código para realizar alguma ação quando um usuário clica em um JButton, passamos um objeto do tipo ActionListener para o método addActionListener do botão e isso, normalmente, é implementado com classes internas. A Listagem 12 exibe o código para a criação desse listener.
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("O botão foi pressionado!");
//Realiza alguma ação quando o botão for pressionado
}
});
Apesar de funcionar, o código exibido é muito grande, mas utilizando funções lambda é possível implementar a mesma funcionalidade com um código muito mais enxuto e simples de entender. A Listagem 13 mostra como esse mesmo código seria implementado utilizando funções lambda.
button.addActionListener( (e) -> {
System.out.println("O botão foi pressionado, e o código
executado utiliza uma função lambda!");
//Realiza alguma ação quando o botão for pressionado
});
Praticamente qualquer Listener pode ser escrito utilizando expressões lambda, como os para escrever arquivos em logs e para verificar se um atributo foi adicionado em uma sessão de um usuário em aplicação web.
Exemplos 4 – métodos que aceitam funções lambda como parâmetros
Além de escrever funções lambda, também é possível criar métodos que as recebam como parâmetro, o que é bastante útil e pode tornar um método bastante flexível. Por exemplo, podemos criar um método genérico para imprimir elementos de uma lista, mas passamos como parâmetro a função para a filtragem dos elementos dessa lista, assim, com apenas um método, e passando a função como parâmetro, é possível fazer a filtragem da lista de várias maneiras diferentes. A Listagem 14 mostra um exemplo de como poderia ser feito isso.
package teste.devmedia;
import java.util.Arrays;
import java.util.List;
import java.util.function.IntFunction;
import java.util.function.Predicate;
public class Main {
public static void main(String [] a) {
System.out.println("Cria a lista com os elementos que serão realizadas operações");
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
System.out.println("Imprime todos os números:");
avaliaExpressao(list, (n)->true);
System.out.println("Não imprime nenhum número:");
avaliaExpressao(list, (n)->false);
System.out.println("Imprime apenas número pares:");
avaliaExpressao(list, (n)-> n%2 == 0 );
System.out.println("Imprime apenas números impares:");
avaliaExpressao(list, (n)-> n%2 == 1 );
System.out.println("Imprime apenas números maiores que 5:");
avaliaExpressao(list, (n)-> n > 5 );
System.out.println("Imprime apenas números maiores que 5 e menores que 10:");
avaliaExpressao(list, (n)-> n > 5 && n < 10);
}
public static void avaliaExpressao(List<Integer> list, Predicate<Integer> predicate) {
list.forEach(n -> {
if(predicate.test(n)) {
System.out.println(n + " ");
}
});
}
}
O método avaliaExpressao recebe como parâmetro uma lista e um objeto do tipo Predicate, que é uma interface, que espera uma função logica, isto é, que avalia uma expressão booleana, e retorna true ou false. Essa função é executada chamando o método test, que executará a função passada como parâmetro. Ao ser avaliada, se essa função retornar verdadeiro, imprime o valor da lista, caso contrário, não imprime nada.
A Listagem 15 exibe um código parecido, mas ao invés de um Predicate, a função passada como parâmetro para o método é um IntFunction, que espera funções que são realizadas sobre números inteiros, como soma e multiplicação. No exemplo, é possível observar que dentro do método realizaOperacao, o objeto function chama o método apply, que executa a função lambda passada como parâmetro e, assim como o método anterior, permite muita flexibilidade. Podemos fazer qualquer tipo de operação sobre uma lista de números inteiros.
package teste.devmedia;
import java.util.Arrays;
import java.util.List;
import java.util.function.IntFunction;
import java.util.function.Predicate;
public class Main {
public static void main(String [] a) {
System.out.println("Cria a lista com os elementos que serão realizadas operações");
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
System.out.println("Multiplica todos os elementos da lista por 5:");
realizaOperacao(list, (n)-> n * 5);
System.out.println("Calcula o quadrado de todos os elementos da lista:");
realizaOperacao(list, (n)-> n * n);
System.out.println("Soma 3 em todos os elementos da lista:");
realizaOperacao(list, (n)-> n + 3);
System.out.println("Coloca 0 em todos os elementos da lista:");
realizaOperacao(list, (n)-> 0);
}
public static void realizaOperacao(List<Integer> list, IntFunction<Integer> function) {
list.forEach(n -> {
n = function.apply(n);
System.out.println(n + " ");
});
}
}
Assim como o Predicate e o IntFunction, existem diversas outras interfaces que podem ser utilizadas para utilizar funções lambada. Todas elas têm o mesmo objetivo, que é receber uma função como parâmetro, mas a diferença entre essas interfaces são o tipo e o número de parâmetros de entrada esperados e o tipo de retorno da função.
As funções lambda são mecanismos bastante poderosos, que facilitam muito a escrita de código conciso e evitam que o programador seja obrigado a escrever um monte de código “inútil”, principalmente em operações simples, além de flexibilizar o mesmo.
Apesar de serem bastante úteis, as funções lambda nem sempre são a melhor opção, caso seja necessário reutilizar diversas vezes uma função, talvez seja melhor criar uma classe ou interface com apenas um método e não uma expressão lambda.
Espero que esse artigo tenha sido útil, até a próxima!