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+).

Você vai aprender
  • 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
FormaExemploRegra
Sem parâmetros() -> System.out.println("Olá!")Parênteses vazios são obrigatórios.
Um parâmetrox -> x * 2Os parênteses são opcionais.
Um parâmetro, com parênteses(x) -> x * 3O mesmo que o anterior.
Com o tipo(int x) -> x * xCom tipo, os parênteses são obrigatórios. Também vale (var x) -> ….
Dois ou mais(a, b) -> a + bParê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.
Expressão ou bloco?

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 Object ou var: 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étodoA lambda recebe……e devolveO método faz
lista.forEach(x -> …)cada elementonadaExecuta a ação para cada um.
lista.replaceAll(x -> …)cada elementoo novo valorTroca cada elemento pelo resultado.
lista.removeIf(x -> …)cada elementotrue/falseRemove os que deram true (veja Iterator).
lista.sort((a, b) -> …)dois elementosnegativo/zero/positivoOrdena (veja Comparator).
mapa.forEach((k, v) -> …)chave e valornadaExecuta 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çãoCódigo
Um for-each comum (sem lambda)for (int n : notas) { soma += n; }
Um stream, que soma sozinhoint 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.

Atributos podem mudar

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

"local variables referenced from a lambda expression must be final or effectively final"

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.

"not a statement": bloco sem 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.

"incompatible parameter types in lambda expression": número de parâmetros errado

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) -> ….

"Object is not a functional interface"

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

FormaExemploEquivale a um método…
() -> expr() -> 42sem parâmetros que devolve 42
() -> { … }() -> System.out.println("oi")sem parâmetros e sem retorno
x -> exprx -> x * 2de um parâmetro
(a, b) -> expr(a, b) -> a + bde dois parâmetros
(int a, int b) -> expr(int a, int b) -> a * bcom 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:

Transformacao dobro = ;

2. Sem parâmetros

Complete a lambda de uma Acao (sem parâmetros) que mostra "Oi":

Acao oi = ;

3. Para cada um

Complete para mostrar cada nome da lista nomes:

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.