A criação de Funções Lambda foi a principal novidade do Java 8, lançado em 2014! Hoje é praticamente obrigatório conhecer como elas funcionam e saber utilizá-las no seu código.
Prefere esse conteúdo em vídeo? Assista aqui!
Java 8 e programação funcional
Há algum tempo o JavaScript veio se estabelecendo como a linguagem padrão de desenvolvimento front-end. Ao mesmo tempo ocorre também o aparecimento e a popularização de linguagens como Scala, Kotlin e Python. Junto desses movimentos, a programação funcional começou a se tornar cada vez mais popular.
Com a intenção de trazer essa possibilidade também para o Java, foi criada a nova sintaxe de funções lambda. Se você nunca viu uma, aqui está:
() -> System.out.println("Hello World")
Se você nunca viu um código assim, não se assuste, você vai entender facilmente o que significa.
Java sem funções lambda
Se você programa Java há algum tempo, provavelmente já teve que escrever algum código parecido com esse:
public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println("Hello World"); } }).run(); }
Apesar de esse código funcionar perfeitamente, ele tem um grande problema: é gigante. Pense bem, foram necessárias 6 linhas de código para criar uma Thread e imprimir “Hello World”, algo que deveria ser completamente trivial. Agora, veja esse mesmo código utilizando uma função lambda:
public static void main(String[] args) { new Thread(() -> System.out.println("Hello World")).run(); }
O código acima é interpretado pelo compilador exatamente igual ao anterior, tendo uma única linha de código e sendo muito mais conciso. E o mais interessante aqui é que ainda estamos utilizando o mesmo construtor para criar uma Thread
. A própria IDE confirma isso:
O compilador sabe que essa função lambda é uma instância de Runnable
, mesmo que nós não deixemos isso explícito. Ok, mas como ele sabe disso?
Java 8 e o conceito SAM
Esse comportamento do compilador fica simples quando entendemos o conceito de Single Abstract Method, ou SAM. Basicamente, qualquer interface que tenha um único método está seguindo esse conceito. Logo, o compilador entende que sua função lambda é, na verdade, a implementação desse único método. Vamos ver de perto.
A interface Runnable
, por exemplo, possui apenas o método run
. Aqui está ela, copiada direto da JDK:
@FunctionalInterface public interface Runnable { public abstract void run(); }
Sendo assim, ao utilizar uma função lambda como fizemos acima, o compilador entende que ela só pode ser a implementação do método run
.
Quanto à anotação @FunctionalInterface
presente nessa classe, ela é apenas informativa. Ela instrui ao compilador que gere um erro caso essa classe não preencha todos os requisitos para ser uma interface funcional, ou seja, uma que pode ser criada a partir de uma função lambda. Por exemplo, se essa interface tivesse dois métodos, ocorreria um erro de compilação. Apesar disso, essa anotação não é obrigatória. Você pode utilizar funções lambda com qualquer interface que atenda os pré-requisitos para ser considerada funcional.
Ok, então funções lambda servem apenas para eu diminuir a quantidade de linhas de código em casos como esse? Não. A verdade é que as funções lambda são muito mais úteis do que parecem. Os exemplos que dei acima são apenas para entender seu funcionamento, mas o que elas permitem fazer em Java é muito mais interessante.
Programação funcional
Com funções lambda é possível utilizar métodos muito conhecidos para quem utiliza JavaScript, como filter
, map
e forEach
.
Imagine, por exemplo, que você tem uma lista de números. Você deseja imprimir o valor dos 7 primeiros, multiplicado por 2, mas apenas se o número for par. Vejamos uma implementação possível com Java tradicional:
public static void main(String[] args) { List<Integer> lista = Arrays.asList(1,5,8,7,4,6,3,2,1,8,5,7,4); for (int i = 0; i < 7; i++) { Integer numero = lista.get(i); if (numero % 2 == 0) { System.out.println(numero * 2); } } }
Essa é uma implementação comum, funciona perfeitamente e, se você programa em Java há algum tempo, provavelmente está acostumado a vê-la. Porém, vamos ver como seria a mesma implementação com o uso de Streams do Java 8:
public static void main(String[] args) { List<Integer> lista = Arrays.asList(1,5,8,7,4,6,3,2,1,8,5,7,4); lista.stream() .limit(7) .filter(e -> e % 2 == 0) .map(e -> e * 2) .forEach(System.out::println); }
Para quem não está acostumado, essa implementação pode parecer estranha a primeira vez. Porém, ela é muito mais concisa e delimitada. É possível saber exatamente todas as operações que estão sendo feitas nessa lista. Claramente existe um limite (limit
), um filtro (filter
), uma transformação (map
) e uma ação para cada item (forEach
).
Essa é estrutura da função lambda passada para o método filter
: