Java Lambdas
Uma lambda é uma função sem nome, escrita no lugar onde vai ser usada: x -> x * 2 quer dizer "dado x, devolva x * 2". Com ela, você passa um comportamento para um método do mesmo jeito que passa um número ou um texto. Você já usou algumas nas aulas de coleções (removeIf(n -> n < 5)); agora vai entender cada pedaço. É a porta de entrada da programação funcional em Java (Java 8+).
- O que é uma lambda e por que ela existe (o "antes": a classe anônima).
- A sintaxe, em todas as formas: sem parâmetro, um, dois, com tipo, com bloco e
return. - Onde uma lambda pode ser usada: no lugar de uma interface funcional.
- A regra das variáveis "efetivamente final" e o erro real do
javac. - Lambdas com coleções:
forEach,removeIf,replaceAll,sort.
A ideia: um bilhete com instruções
A Ana vai viajar e deixa a turma com uma professora substituta. Em vez de dar à colega um manual inteiro com capa, índice e o nome da autora, ela deixa um bilhete: "para cada aluno, se a nota for menor que 5, marque recuperação". O bilhete não tem nome nem capa: é só a instrução.
Em Java, antes do Java 8, para passar uma instrução para um método você precisava escrever um "manual": uma classe inteira (às vezes anônima) com um método dentro. A lambda é o bilhete: só os parâmetros, uma seta e o que fazer.
Antes e depois: classe anônima × lambda
Vamos usar uma interface bem simples, com um único método: uma operação que recebe dois números e devolve um.
interface Operacao {
int aplicar(int a, int b);
}
public class Main {
public static void main(String[] args) {
// 1. classe anônima
Operacao soma = new Operacao() {
@Override
public int aplicar(int a, int b) {
return a + b;
}
};
// 2. lambda
Operacao multiplica = (a, b) -> a * b;
System.out.println("soma: " + soma.aplicar(7, 3));
System.out.println("multiplica: " + multiplica.aplicar(7, 3));
System.out.println("calcular: " + calcular(7, 3, (a, b) -> a - b));
}
static int calcular(int x, int y, Operacao op) {
return op.aplicar(x, y);
}
}
$ javac Main.java $ java Main soma: 10 multiplica: 21 calcular: 4
A classe anônima (forma 1) tem 6 linhas, e só uma delas importa: return a + b;. A lambda (forma 2) diz a mesma coisa em uma linha. Compare as peças:
new Operacao() { « o compilador já sabe que é uma Operacao
@Override
public int aplicar(int a, int b) { « e que o único método recebe (int, int)
return a + b; « o que realmente importa
}
};
(a, b) -> a + b « só o que importa
A lambda funciona porque o compilador deduz o resto pelo contexto: a variável é do tipo Operacao, Operacao tem um só método abstrato, esse método recebe dois int e devolve um int. Então (a, b) -> a * b só pode ser a implementação de aplicar, com a e b do tipo int.
A terceira linha mostra a maior vantagem: a lambda foi escrita direto no argumento de calcular. O método calcular não sabe que conta vai fazer; quem chama é que decide, passando o "bilhete".
A sintaxe, peça por peça
- (a, b)
- Os parâmetros, como os de um método, mas em geral sem o tipo (o compilador deduz).
- ->
- A seta (hífen + sinal de maior). Lê-se "vai para" ou "resulta em".
- a + b
- O corpo. Se for uma expressão só, o valor dela é devolvido automaticamente, sem escrever
return.
Todas as formas possíveis:
interface Acao {
void executar();
}
interface Transformacao {
int aplicar(int x);
}
interface Operacao {
int aplicar(int a, int b);
}
public class Main {
public static void main(String[] args) {
Acao ola = () -> System.out.println("Olá!");
Transformacao dobro = x -> x * 2;
Transformacao triplo = (x) -> x * 3;
Transformacao quadrado = (int x) -> x * x;
Operacao soma = (a, b) -> a + b;
Operacao media = (a, b) -> {
int total = a + b;
return total / 2;
};
ola.executar();
System.out.println(dobro.aplicar(5));
System.out.println(triplo.aplicar(5));
System.out.println(quadrado.aplicar(5));
System.out.println(soma.aplicar(7, 4));
System.out.println(media.aplicar(7, 4));
}
}
$ javac Main.java $ java Main Olá! 10 15 25 11 5 « 11 / 2 com int: divisão inteira
| Forma | Exemplo | Regra |
|---|---|---|
| Sem parâmetros | () -> System.out.println("Olá!") | Parênteses vazios são obrigatórios. |
| Um parâmetro | x -> x * 2 | Os parênteses são opcionais. |
| Um parâmetro, com parênteses | (x) -> x * 3 | O mesmo que o anterior. |
| Com o tipo | (int x) -> x * x | Com tipo, os parênteses são obrigatórios. Também vale (var x) -> …. |
| Dois ou mais | (a, b) -> a + b | Parênteses obrigatórios. |
| Corpo com bloco | (a, b) -> { int t = a + b; return t / 2; } | Com chaves, é um bloco normal: precisa de return (se devolve algo) e de ; em cada instrução. |
Se a lambda cabe numa expressão, escreva sem chaves: x -> x * 2. Se precisa de várias linhas, use o bloco com return. Se o bloco ficou grande (mais de 3 ou 4 linhas), é sinal de que aquilo merece virar um método com nome; aí você pode até usar uma referência de método.
Missão: complete a lambda maior para devolver o maior dos dois números (dica: o operador ternário a > b ? a : b). A saída deve ser maior(7, 10) = 10 e maior(8, 2) = 8.
interface Operacao {
int aplicar(int a, int b);
}
public class Main {
public static void main(String[] args) {
Operacao maior = (a, b) -> 0;
System.out.println("maior(7, 10) = " + maior.aplicar(7, 10));
System.out.println("maior(8, 2) = " + maior.aplicar(8, 2));
}
}
interface Operacao {
int aplicar(int a, int b);
}
public class Main {
public static void main(String[] args) {
Operacao maior = (a, b) -> a > b ? a : b;
System.out.println("maior(7, 10) = " + maior.aplicar(7, 10));
System.out.println("maior(8, 2) = " + maior.aplicar(8, 2));
}
}
Onde uma lambda pode ser usada
Uma lambda não existe sozinha: ela sempre vira um objeto de uma interface funcional, ou seja, uma interface com exatamente um método abstrato (como Operacao, Acao, Comparator, Runnable). O tipo da variável ou do parâmetro diz qual é. Duas consequências:
- A lambda precisa "encaixar" no método: mesmo número de parâmetros e retorno compatível.
- Não dá para guardar uma lambda numa variável
Objectouvar: o compilador não saberia qual interface ela implementa.
Você não precisa criar uma interface para cada caso: o Java já traz dezenas prontas (Function, Predicate, Consumer…). Elas são o assunto da próxima aula, Interfaces Funcionais.
Lambdas com coleções
É com coleções que as lambdas aparecem mais no dia a dia. Muitos métodos recebem um "bilhete" dizendo o que fazer com cada elemento:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> nomes = new ArrayList<>(List.of("carla", "ana", "bruno", "edu"));
nomes.forEach(n -> System.out.println("Olá, " + n));
nomes.replaceAll(n -> n.toUpperCase());
System.out.println("replaceAll: " + nomes);
nomes.removeIf(n -> n.length() <= 3);
System.out.println("removeIf: " + nomes);
nomes.sort((a, b) -> a.compareTo(b));
System.out.println("sort: " + nomes);
Map<String, Double> notas = new TreeMap<>(Map.of("Ana", 9.5, "Bruno", 7.0));
notas.forEach((nome, nota) -> System.out.println(nome + " tirou " + nota));
}
}
$ javac Main.java $ java Main Olá, carla Olá, ana Olá, bruno Olá, edu replaceAll: [CARLA, ANA, BRUNO, EDU] removeIf: [CARLA, BRUNO] sort: [BRUNO, CARLA] Ana tirou 9.5 Bruno tirou 7.0
| Método | A lambda recebe… | …e devolve | O método faz |
|---|---|---|---|
lista.forEach(x -> …) | cada elemento | nada | Executa a ação para cada um. |
lista.replaceAll(x -> …) | cada elemento | o novo valor | Troca cada elemento pelo resultado. |
lista.removeIf(x -> …) | cada elemento | true/false | Remove os que deram true (veja Iterator). |
lista.sort((a, b) -> …) | dois elementos | negativo/zero/positivo | Ordena (veja Comparator). |
mapa.forEach((k, v) -> …) | chave e valor | nada | Executa para cada par. |
E o próximo passo, os Streams, é praticamente feito só de lambdas: filter(n -> n >= 7), map(n -> n * 2)…
Variáveis de fora: "efetivamente final"
Uma lambda pode usar variáveis locais do método onde ela foi escrita. Isso se chama capturar a variável:
int bonus = 1;
Transformacao comBonus = nota -> nota + bonus;
System.out.println(comBonus.aplicar(8));
System.out.println(comBonus.aplicar(6));
$ java Main 9 7
Mas há uma regra: a variável capturada precisa ser final ou "efetivamente final" (effectively final), isto é, nunca pode mudar de valor depois de criada, nem antes nem depois da lambda. Se a Ana mudar o bônus depois:
Este código tem um erro de propósito.
interface Transformacao {
int aplicar(int x);
}
public class Main {
public static void main(String[] args) {
int bonus = 1;
Transformacao comBonus = nota -> nota + bonus;
bonus = 2;
System.out.println(comBonus.aplicar(8));
}
}
$ javac Main.java
Main.java:8: error: local variables referenced from a lambda expression must be final or effectively final
Transformacao comBonus = nota -> nota + bonus;
^
1 error
"Variáveis locais referenciadas por uma expressão lambda precisam ser final ou efetivamente final." Repare que o erro aponta a linha da lambda (8), embora o "culpado" seja o bonus = 2 da linha 9: foi essa linha que tirou do bonus o status de "efetivamente final".
Por que essa regra? A lambda pode ser executada depois, até em outro lugar do programa (em outra thread, por exemplo). Ela guarda uma cópia do valor da variável. Se a variável pudesse mudar, ficaria a dúvida: a lambda usa o valor antigo ou o novo? O Java elimina a dúvida proibindo a mudança.
O caso mais comum: somar dentro de um forEach
Quem começa com lambdas logo tenta isto:
Este código tem um erro de propósito.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> notas = List.of(7, 10, 3);
int soma = 0;
notas.forEach(n -> soma += n);
System.out.println(soma);
}
}
$ javac Main.java
Main.java:7: error: local variables referenced from a lambda expression must be final or effectively final
notas.forEach(n -> soma += n);
^
1 error
A lambda tenta mudar soma, e isso é proibido. As soluções, da mais simples à mais moderna:
| Solução | Código |
|---|---|
| Um for-each comum (sem lambda) | for (int n : notas) { soma += n; } |
| Um stream, que soma sozinho | int soma = notas.stream().mapToInt(n -> n).sum(); |
| Um atributo da classe (não é variável local) | static int soma; fora do main |
Laços comuns continuam valendo! Lambda é uma ferramenta a mais, não uma obrigação. Use forEach para fazer algo com cada elemento (mostrar, enviar); para acumular um resultado, use um laço ou um stream.
A regra vale para variáveis locais (e parâmetros) do método. Atributos de objetos e variáveis static podem ser lidos e alterados dentro de uma lambda normalmente. E o this numa lambda é o objeto onde ela foi escrita (diferente da classe anônima, onde this é a própria classe anônima).
Quando dá errado
O que aconteceu: a lambda usa uma variável local que muda de valor em algum lugar do método (dentro ou fora da lambda).
Como consertar: não mude a variável (crie outra, final int bonusAtual = bonus;, e use essa na lambda), ou troque o forEach por um laço comum ou um stream.
return
Este código tem um erro de propósito.
Operacao media = (a, b) -> {
int total = a + b;
total / 2;
};
$ javac Main.java
Main.java:9: error: not a statement
total / 2;
^
1 error
O que quer dizer: "isto não é uma instrução". Dentro de chaves, a lambda é um bloco comum, e uma conta solta não faz nada.
Como consertar: return total / 2;. Ou, se cabe numa expressão, tire as chaves: (a, b) -> (a + b) / 2.
Este código tem um erro de propósito.
Operacao dobro = x -> x * 2;
$ javac Main.java
Main.java:7: error: incompatible types: incompatible parameter types in lambda expression
Operacao dobro = x -> x * 2;
^
1 error
O que quer dizer: a lambda tem um parâmetro, mas o método de Operacao recebe dois.
Como consertar: use a interface certa (uma com um parâmetro só, como Transformacao), ou escreva (a, b) -> ….
Este código tem um erro de propósito.
Object o = () -> 1;
$ javac Main.java
Main.java:3: error: incompatible types: Object is not a functional interface
Object o = () -> 1;
^
1 error
O que quer dizer: uma lambda só pode virar uma interface funcional (com um método abstrato). Object é uma classe.
Como consertar: declare a variável com a interface: Operacao, Runnable, Supplier<Integer>…
Referência
| Forma | Exemplo | Equivale a um método… |
|---|---|---|
() -> expr | () -> 42 | sem parâmetros que devolve 42 |
() -> { … } | () -> System.out.println("oi") | sem parâmetros e sem retorno |
x -> expr | x -> x * 2 | de um parâmetro |
(a, b) -> expr | (a, b) -> a + b | de dois parâmetros |
(int a, int b) -> expr | (int a, int b) -> a * b | com os tipos escritos |
(a, b) -> { …; return r; } | (a, b) -> { int t = a + b; return t / 2; } | com várias instruções |
Exercícios
1. O dobro
Complete a lambda que recebe x e devolve o dobro:
2. Sem parâmetros
Complete a lambda de uma Acao (sem parâmetros) que mostra "Oi":
3. Para cada um
Complete para mostrar cada nome da lista nomes:
4. Qual destas lambdas não compila?
5. int total = 0; lista.forEach(x -> total += x); …
6. Uma lambda pode ser guardada numa variável de qual tipo?
Missão final: usando só lambdas (sem for): remova da lista os nomes com menos de 4 letras com removeIf e depois cumprimente cada um com forEach. A saída deve ser Olá, Bruno! e Olá, Carla!.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> nomes = new ArrayList<>(List.of("Ana", "Bruno", "Edu", "Carla"));
}
}
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> nomes = new ArrayList<>(List.of("Ana", "Bruno", "Edu", "Carla"));
nomes.removeIf(n -> n.length() < 4);
nomes.forEach(n -> System.out.println("Olá, " + n + "!"));
}
}
Resumo
- Uma lambda é uma função sem nome:
parâmetros -> corpo. - Ela substitui a classe anônima quando a interface tem um único método abstrato (uma interface funcional).
- Formas:
() -> …,x -> …,(a, b) -> …,(int x) -> …, e bloco{ …; return …; }. - O tipo dos parâmetros e o retorno são deduzidos pela interface do contexto.
- Variáveis locais usadas na lambda precisam ser efetivamente final; para acumular, use um laço ou um stream.
- Com coleções:
forEach,replaceAll,removeIf,sort,Map.forEach.
Próxima aula: Interfaces Funcionais, as interfaces prontas do Java para receber lambdas.