CodeGym /Cursos /JAVA 25 SELF /Introdução às expressões lambda

Introdução às expressões lambda

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

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
Runnable
() -> System.out.println("Hi")
new Runnable() { public void run() { System.out.println("Hi"); } }
Consumer<String>
s -> System.out.println(s)
new Consumer<String>() { public void accept(String s) { System.out.println(s); } }
Comparator<Integer>
(a, b) -> a - b
new Comparator<Integer>() { public int compare(Integer a, Integer b) { return a - b; } }

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;
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION