23 de março de 2019

Java 8: Entenda facilmente funções lambda, a principal novidade!

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:

Java 8: Screenshot do Eclipse mostrando o construtor da classe Thread recebendo uma instância da interface Runnable.
Construtor recebendo uma instância de Runnable

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 @FunctionalInterfacepresente 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:

Share