CodeGym /Cursos /JAVA 25 SELF /Expressões lambda: sintaxe e escopos

Expressões lambda: sintaxe e escopos

JAVA 25 SELF
Nível 48 , Lição 0
Disponível

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
() -> System.out.println("Hi")
Por exemplo, para Runnable
Um parâmetro
x -> x * x
Pode ser sem parênteses
Vários parâmetros
(a, b) -> a + b
Parênteses obrigatórios
Uma expressão
x -> x + 1
Sem return e sem chaves
Bloco de código
x -> { int y = x + 1; return y * 2; }
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.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION