As expressões lambda chegaram no Java 8 e mudaram a forma de escrever código na linguagem. Elas permitem passar comportamento como se fosse um valor: em vez de criar uma classe só para implementar um método, você escreve a função diretamente onde ela é usada.
Antes das lambdas
Para ordenar uma lista de nomes pelo tamanho, era preciso uma classe anônima:
List<String> nomes = new ArrayList<>(List.of("Ana", "Bernardo", "Lu"));
Collections.sort(nomes, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
});Com uma lambda, o mesmo código fica assim:
nomes.sort((a, b) -> Integer.compare(a.length(), b.length()));A sintaxe
Uma lambda tem três partes: os parâmetros, a seta -> e o corpo.
// sem parâmetros
Runnable r = () -> System.out.println("Olá!");
// um parâmetro: os parênteses são opcionais
Consumer<String> imprimir = texto -> System.out.println(texto);
// vários parâmetros
BinaryOperator<Integer> somar = (a, b) -> a + b;
// corpo com várias linhas: use chaves e return
Function<Integer, String> classificar = n -> {
if (n < 0) return "negativo";
return n == 0 ? "zero" : "positivo";
};Interfaces funcionais
Uma lambda sempre implementa uma interface funcional: uma interface com um único método abstrato. O pacote java.util.function já traz as mais usadas:
| Interface | Recebe | Devolve | Exemplo |
|---|---|---|---|
Predicate<T> | T | boolean | n -> n > 18 |
Function<T, R> | T | R | s -> s.length() |
Consumer<T> | T | nada | s -> System.out.println(s) |
Supplier<T> | nada | T | () -> new ArrayList<>() |
BinaryOperator<T> | T, T | T | (a, b) -> a * b |
Você também pode criar as suas. A anotação @FunctionalInterface é opcional, mas faz o compilador avisar se alguém adicionar um segundo método abstrato:
@FunctionalInterface
interface Desconto {
double aplicar(double valor);
}
Desconto blackFriday = valor -> valor * 0.7;
System.out.println(blackFriday.aplicar(100)); // 70.0Method references
Quando a lambda só chama um método que já existe, dá para encurtar ainda mais com :::
nomes.forEach(nome -> System.out.println(nome));
nomes.forEach(System.out::println); // equivalente
nomes.stream().map(String::toUpperCase); // método de instância
Supplier<List<String>> nova = ArrayList::new; // construtorLambdas com Streams
É com a API de Streams que as lambdas mostram o seu valor. Filtrar, transformar e agrupar dados vira uma sequência de passos legíveis:
record Aluno(String nome, double nota) {}
List<Aluno> alunos = List.of(
new Aluno("Ana", 9.0),
new Aluno("Caio", 5.5),
new Aluno("Bia", 7.8));
List<String> aprovados = alunos.stream()
.filter(a -> a.nota() >= 7)
.sorted(Comparator.comparing(Aluno::nota).reversed())
.map(Aluno::nome)
.toList();
System.out.println(aprovados); // [Ana, Bia]Cuidados
- Variáveis externas precisam ser efetivamente finais. Uma lambda pode ler uma variável local de fora, mas não alterá-la.
- Lambdas longas escondem a lógica. Se passou de três ou quatro linhas, extraia para um método com nome e use method reference.
- Exceções checadas não combinam bem com as interfaces de
java.util.function: trate a exceção dentro da lambda ou crie a sua própria interface.