1. Apresentação
Em poucas palavras: uma expressão lambda é uma forma rápida de criar uma implementação de uma interface funcional “na hora”, sem declarar uma classe separada ou uma classe anônima. É como um mini-método sem nome que pode ser passado como argumento ou guardado em uma variável.
Por muito tempo, o Java foi uma linguagem OO “clássica”. Se você quisesse passar um trecho de comportamento, por exemplo, o que fazer ao clicar em um botão, precisava escrever classes anônimas:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick() {
System.out.println("Botão clicado!");
}
});
Desde o Java 8 surgiu a “mágica” das lambdas:
button.setOnClickListener(() -> System.out.println("Botão clicado!"));
Onde () -> System.out.println("Botão clicado!") é justamente uma expressão lambda.
Formalmente: uma expressão lambda é uma forma compacta de escrever a implementação do único método abstrato de uma interface funcional.
Por que isso é importante?
- Permite passar comportamento como valor (por exemplo, o que fazer com cada elemento de uma lista).
- Ajuda a escrever código mais compacto e legível.
- Facilita o uso de APIs modernas do Java: Stream API, tratamento de eventos, tarefas assíncronas e muito mais.
2. Sintaxe das expressões lambda
Forma geral
(parametry) -> vyrazhenie
// ou
(parametry) -> { blok koda }
Exemplos para diferentes interfaces
1. Sem parâmetros (por exemplo, Runnable):
Runnable r = () -> System.out.println("Olá, lambda!");
r.run();
2. Um parâmetro (por exemplo, Consumer):
Consumer<String> printer = s -> System.out.println("Você passou: " + s);
printer.accept("Java");
Se houver apenas um parâmetro e seu tipo puder ser inferido, os parênteses podem ser omitidos.
O tipo String é o tipo da variável (s) que passamos ali.
3. Vários parâmetros (por exemplo, Comparator):
Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5
O tipo Integer é o tipo das variáveis (a, b) que passamos ali.
4. Corpo de múltiplas linhas (são necessárias chaves e return, se houver retorno explícito):
Function<Integer, Integer> square = x -> {
int result = x * x;
return result;
};
System.out.println(square.apply(6)); // 36
O primeiro Integer é o tipo do resultado da função; o segundo Integer é o tipo do parâmetro.
Atalhos e concisão
- Se o corpo for uma única expressão, as chaves e o return podem ser omitidos.
- Se não houver parâmetros — use parênteses vazios: () -> ...
- Se houver um parâmetro — pode ser sem parênteses: x -> ...
- Se houver mais de um parâmetro — são necessários parênteses: (a, b) -> ...
Tabela-resumo
Veja como a mesma ideia é escrita com lambda e com classe anônima:
| Interface | Expressão lambda (exemplo) | Equivalente com classe anônima |
|---|---|---|
|
|
|
|
|
|
|
|
|
Exemplo com uma interface própria
Suponha que temos uma interface funcional:
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
Antes era assim:
Operation sum = new Operation() {
@Override
public void apply(int a, int b) {
return a + b;
}
};
Agora — do jeito moderno:
Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8
Exemplos para interfaces padrão
Runnable:
Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();
Comparator:
List<String> list = Arrays.asList("maçã", "banana", "kiwi");
list.sort((a, b) -> a.length() - b.length());
System.out.println(list);
Function:
Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123
3. Escopo e captura de variáveis
Variáveis do contexto externo (effectively final)
Expressões lambda podem usar variáveis do método ao redor. Mas há uma regra: essas variáveis devem ser effectively final — ou seja, ou são explicitamente declaradas como final, ou simplesmente não são modificadas após a inicialização.
Exemplo:
String prefix = "Resultado: ";
Function<Integer, String> f = x -> prefix + (x * 2);
// prefix aqui está "congelado" — depois disso não pode ser alterado
System.out.println(f.apply(5)); // Resultado: 10
Se você tentar modificar prefix depois que ele for usado na lambda, o compilador emitirá um erro.
Por que assim?
Uma expressão lambda pode ser chamada após a saída do método no qual a variável foi declarada. Para evitar bugs “mágicos”, o Java permite usar apenas variáveis imutáveis.
Diferença em relação às classes anônimas
Nas classes anônimas, vale a mesma regra: variáveis do método externo devem ser final/effectively final. Mas há nuances de escopo: dentro de uma classe anônima, this se refere à própria classe anônima; já em uma lambda, refere-se à classe externa.
public class Demo {
public void test() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(this); // Irá imprimir: Demo$1 (classe anônima)
}
};
Runnable r2 = () -> System.out.println(this); // Irá imprimir: Demo (classe externa)
r1.run();
r2.run();
}
}
Lambda e campos da classe
Uma expressão lambda pode acessar os campos da classe externa sem restrições:
public class Counter {
private int base = 10;
public void printSum(int x) {
Function<Integer, Integer> sum = y -> base + y + x;
System.out.println(sum.apply(5));
}
}
Aqui, base — pode ser alterado (é um campo da classe).
x — deve ser effectively final.
4. Prática: vamos escrever algumas expressões lambda
Exemplo: filtragem de uma lista de números
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
nums.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> System.out.println("Par: " + n));
Você aprenderá mais sobre a Stream API no nível 30 :P
Exemplo: função de transformação de string
Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA
Exemplo: sua própria interface funcional
@FunctionalInterface
interface StringTransformer {
String transform(String s);
}
StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("Olá")); // Olá!
Exemplo: usando variáveis do contexto externo
int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// factor não pode ser alterado depois disso!
5. Erros comuns ao trabalhar com expressões lambda
Erro nº 1: tentar modificar uma variável capturada pela lambda.
Esse código não compilará — a variável deve ser effectively final:
int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // Erro: sum não é final!
Se você precisa acumular valores, use um array ou um wrapper (objeto contêiner).
Erro nº 2: confusão com o escopo de this.
Dentro de uma expressão lambda, this se refere à classe externa, e não à lambda (diferente de uma classe anônima).
Erro nº 3: esquecer as chaves e o return em uma lambda de múltiplas linhas.
Se o corpo da lambda não for uma única expressão, são necessárias chaves e return:
Function<Integer, Integer> square = x -> {
int y = x * x;
return y;
};
Erro nº 4: definição incorreta/indefinida do tipo da expressão lambda.
Uma expressão lambda sempre implementa uma interface funcional. Não é possível simplesmente escrever:
var f = x -> x + 1; // Erro! Tipo de interface desconhecido.
É preciso declarar o tipo explicitamente:
Function<Integer, Integer> f = x -> x + 1;
GO TO FULL VERSION