1. Introdução
Sejamos honestos: escrever classes anônimas por causa de uma ou duas linhas de código — é como contratar um caminhão enorme para levar um único pãozinho da padaria para a loja.
Por exemplo, se você quiser ordenar uma lista de strings pelo comprimento, antes do Java 8 era preciso escrever assim:
List<String> list = Arrays.asList("maçã", "banana", "kiwi");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
A tarefa é simples, mas o código ocupa meia tela. Isso irrita especialmente quando há muitas dessas operações: o código fica “barulhento” e o objetivo se perde. Programadores choraram, sofreram e então inventaram as expressões lambda. Já estudamos um pouco; agora vamos revisar e aprofundar nosso conhecimento.
O que é uma expressão lambda
Uma expressão lambda é uma forma compacta de escrever a implementação de uma interface funcional, ou seja, uma interface com um único método abstrato (por exemplo, Comparator, Runnable, Consumer e muitas outras).
Em outras palavras, uma expressão lambda permite escrever uma função “na hora”, exatamente onde ela é necessária, sem declarar uma classe ou método separados.
Sintaxe geral:
(parametry) -> { telo }
Exemplos:
- (a, b) -> a + b — função que soma dois números
- x -> x * x — função que eleva um número ao quadrado
- () -> System.out.println("Hello!") — função sem parâmetros
Relação com interfaces funcionais:
Uma expressão lambda sempre pode ser atribuída a uma variável de tipo interface funcional ou passada como argumento para um método que espera tal interface.
2. Sintaxe de expressões lambda
Sem parâmetros
Runnable r = () -> System.out.println("Olá, mundo!");
r.run(); // Imprime: Olá, mundo!
Um parâmetro
Se houver apenas um parâmetro, os parênteses podem ser omitidos:
Consumer<String> print = s -> System.out.println(s);
print.accept("Java é demais!");
Vários parâmetros
Os parênteses são obrigatórios:
Comparator<String> cmp = (a, b) -> a.length() - b.length();
Corpo com uma única expressão
Se o corpo consiste de uma única expressão, as chaves e o return não são necessários:
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
Corpo em bloco
Se você precisa de várias instruções, use chaves e return (se houver valor de retorno):
Function<Integer, Integer> abs = x -> {
if (x < 0) {
return -x;
}
return x;
};
System.out.println(abs.apply(-3)); // 3
Tipos de parâmetros
Na maioria das vezes, os tipos de parâmetros podem ser omitidos — o compilador os infere pelo contexto. Mas, se quiser, você pode declará-los explicitamente:
Comparator<String> cmp = (String a, String b) -> a.length() - b.length();
Lambda sem valor de retorno
Se a interface retorna void, apenas escreva as instruções:
list.forEach(s -> System.out.println("Elemento: " + s));
Tabela: Variações de sintaxe de expressões lambda
| O que fazemos | Exemplo | Comentário |
|---|---|---|
| Sem parâmetros | |
Por exemplo, para Runnable |
| Um parâmetro | |
Pode ser sem parênteses |
| Vários parâmetros | |
Parênteses obrigatórios |
| Uma expressão | |
Sem return e sem chaves |
| Bloco de código | |
Com return, se houver resultado |
3. Aplicação: onde e como usar expressões lambda
Expressões lambda são usadas principalmente onde é necessário passar um “comportamento” — uma função — como argumento. Isso foi uma verdadeira revolução para coleções, streams (Stream API), eventos e muito mais.
Ordenação de lista
Antes do Java 8:
list.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Com expressão lambda:
list.sort((a, b) -> a.length() - b.length());
Criação de thread (Thread)
Thread t = new Thread(() -> System.out.println("Thread iniciada!"));
t.start();
Processamento de coleções
list.forEach(s -> System.out.println(s.toUpperCase()));
Filtragem de lista
List<String> longWords = list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
Exemplo: Evoluindo nosso aplicativo de estudo
Suponha que temos uma lista de usuários:
List<String> users = Arrays.asList("Alice", "Bob", "Charlie");
Exibir todos os usuários cujos nomes têm mais de 4 caracteres:
users.stream()
.filter(name -> name.length() > 4)
.forEach(name -> System.out.println("Usuário: " + name));
4. Escopo de variáveis em expressões lambda
Expressões lambda podem usar variáveis do método envolvente, mas há nuances!
Variáveis e lambdas
Uma lambda em Java pode “capturar” apenas variáveis que não mudam após a inicialização. Se a variável for declarada com final — é óbvio. Mas mesmo que a palavra final não esteja escrita, o compilador verifica por conta própria: o valor muda ou não. Se não, ele a considera “como se fosse final” e permite seu uso na lambda.
Exemplo:
int minLength = 4; // o valor não muda em nenhum lugar
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
Aqui tudo funciona, porque minLength permanece o mesmo número.
Mas, se depois de usá-la na lambda você tentar reatribuir minLength, receberá um erro de compilação:
int minLength = 4;
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
minLength = 10; // Erro! A lambda já “fixou” o valor
Basicamente, a regra é muito simples: a variável capturada por uma lambda deve ser imutável.
Diferença em relação às classes anônimas
Em classes anônimas e em expressões lambda, as variáveis do método externo funcionam do mesmo jeito: apenas final/efetivamente final.
MAS!
Em uma expressão lambda, this se refere ao objeto externo (a instância atual da classe), enquanto em uma classe anônima se refere à instância da própria classe anônima. Isso é importante se, dentro da lambda, você acessar campos ou métodos da classe atual.
Exemplo:
public class Example {
String name = "Classe externa";
void demo() {
Runnable r1 = new Runnable() {
String name = "Classe anônima";
@Override
public void run() {
System.out.println(this.name); // "Classe anônima"
}
};
Runnable r2 = () -> System.out.println(this.name); // "Classe externa"
r1.run();
r2.run();
}
}
5. Erros comuns ao trabalhar com expressões lambda
Erro nº 1: Uso de variável não final/não efetivamente final. Se você decidir alterar a variável depois de tê-la usado em uma lambda, o compilador vai reclamar imediatamente. Isso é feito por segurança: caso contrário, não ficaria claro qual valor da variável deveria ser usado.
Erro nº 2: Confusão com this. Em uma expressão lambda, this é a classe externa, e em uma classe anônima é a própria classe anônima. Se você tentar chamar um método da classe externa de dentro de uma lambda, tudo funcionará; já a partir de uma classe anônima — não (se você contar com o contexto da classe externa).
Erro nº 3: Lambda sem contexto. Uma expressão lambda não pode ser usada sozinha — é preciso atribuí-la a uma variável de uma interface funcional ou passá-la para onde essa interface é esperada. Tentar simplesmente escrever x -> x + 1 fora de contexto causará erro.
Erro nº 4: Lambda complexa demais. Se a expressão lambda ficar maior que 3–5 linhas, ela se torna difícil de ler. Nesses casos, é melhor extrair a lógica para um método separado.
GO TO FULL VERSION