Menu

Expressões lambda em Java: código funcional conciso

O que é uma expressão lambda em Java, a sintaxe da seta, como ela implementa uma interface funcional, referências a métodos e captura de variáveis.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Comportamento que você pode passar adiante

Uma expressão lambda é um bloco de código compacto que você pode entregar a um método, armazenar em uma variável ou retornar, assim como faria com um número ou uma string. Antes das lambdas, passar comportamento significava escrever uma classe inteira (ou uma verbosa classe anônima) só para envolver um método. Uma lambda reduz isso à sua essência: os parâmetros e o corpo.

Uma lambda sempre implementa uma interface funcional, ou seja, uma interface com exatamente um método abstrato (você as conheceu no fim da página sobre interfaces). O compilador descobre a qual interface você se refere a partir do contexto e associa sua lambda a esse único método.

x -> x * 2 é a implementação completa de apply. Sem new, sem corpo de classe, sem nome de método: a interface fornece tudo isso.

A sintaxe da seta

Toda lambda tem o formato parâmetros -> corpo. As partes se ajustam de acordo com o quanto você precisa:

() -> 42                       // sem parâmetros
x -> x + 1                     // um parâmetro, parênteses opcionais
(x, y) -> x + y                // dois ou mais parâmetros exigem parênteses
(int x, int y) -> x + y        // os tipos são opcionais: geralmente inferidos
x -> {                          // um corpo de bloco precisa de chaves e return
    int doubled = x * 2;
    return doubled + 1;
}

Um corpo de expressão única (x -> x + 1) retorna seu valor de forma implícita, sem a palavra-chave return. No momento em que você usa chaves, está escrevendo um bloco normal e precisa usar return explicitamente se o método retornar algo. Um erro comum é misturar os dois: x -> { x + 1 } não compila, porque um bloco precisa de uma instrução (return x + 1;).

As lambdas substituem as classes anônimas

A forma mais clara de ver o que uma lambda oferece é o antes/depois. Ordenar com um Comparator personalizado costumava ser assim:

// antes - classe anônima
names.sort(new Comparator<String>() {
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

A mesma coisa como lambda é uma única linha legível:

Comparator é uma interface funcional (seu único método abstrato é compare), então a lambda se encaixa diretamente. Toda a cerimônia (o new, a classe, a assinatura do método) desaparece, restando apenas a lógica de comparação.

A caixa de ferramentas java.util.function

Você raramente precisa declarar sua própria interface funcional. O pacote java.util.function traz os formatos mais comuns, e quase todas as APIs da biblioteca do Java os aceitam:

  • Function<T, R> - recebe um T, retorna um R (apply)
  • Predicate<T> - recebe um T, retorna um boolean (test)
  • Supplier<T> - não recebe nada, retorna um T (get)
  • Consumer<T> - recebe um T, não retorna nada (accept)

Essas são interfaces genéricas: Function<String, Integer> reutiliza os generics que você viu na página anterior para manter a segurança de tipos. Escolha aquela cujo formato corresponde ao que seu código precisa receber e produzir.

Referências a métodos

Quando uma lambda não faz nada além de chamar um método já existente, você pode substituí-la por uma referência a método usando ::. É o mesmo valor, escrito de forma mais direta:

As referências a métodos vêm em variações: String::toUpperCase (um método de instância chamado sobre cada argumento), Math::max (um método estático), System.out::println (um método sobre um objeto específico) e ArrayList::new (um construtor). Recorra a uma somente quando ela se ler com clareza: se você precisar pensar qual forma se aplica, uma lambda comum já basta.

Captura de variáveis

Uma lambda pode usar variáveis locais do método ao seu redor, mas apenas se elas forem final ou efetivamente final: atribuídas uma vez e nunca alteradas. Java captura o valor no momento em que a lambda é criada, então uma variável que pudesse mudar depois seria ambígua.

Se você reatribuir factor em qualquer lugar, o compilador rejeita a lambda com "variable used in lambda expression should be final or effectively final". Quando você realmente precisar de estado mutável compartilhado, capture um objeto em vez disso (um campo, um elemento de array ou um AtomicInteger), porque a referência permanece fixa mesmo que o conteúdo mude. Observe que as lambdas, diferentemente das classes anônimas, não criam seu próprio escopo: this dentro de uma lambda se refere à instância que a envolve, não à própria lambda.

Próximo: Streams

As lambdas são o bloco de construção, não o destino. Seu verdadeiro retorno vem com a API de Streams, onde você encadeia operações como filter, map e reduce (cada uma recebendo uma lambda) para expressar transformações de dados como um pipeline legível em vez de um emaranhado de laços. É o que veremos na próxima página.

Perguntas frequentes

O que é uma expressão lambda em Java?

Uma lambda é uma forma curta de escrever uma instância de uma interface funcional, ou seja, uma interface com um único método abstrato. Em vez de uma classe anônima inteira, você escreve parâmetros -> corpo. O compilador associa a lambda ao único método da interface, de modo que Runnable r = () -> System.out.println("hi"); é um Runnable completo. As lambdas permitem passar comportamento de um lado para outro como se fossem dados.

Qual é a diferença entre uma lambda e uma referência a método em Java?

Ambas produzem uma instância de uma interface funcional. Uma lambda tem parâmetros explícitos e um corpo (s -> s.toUpperCase()), enquanto uma referência a método é a forma abreviada de uma lambda que apenas chama um método já existente (String::toUpperCase). Use uma referência a método quando a lambda não fizer nada além de encaminhar seus argumentos para um único método nomeado: fica mais curta e mais legível.

Por que as variáveis usadas em uma lambda Java precisam ser final ou efetivamente final?

Uma lambda pode capturar variáveis locais do método que a envolve, mas apenas se elas nunca mudarem após a atribuição; é isso que significa "efetivamente final". Java captura o valor, não uma referência viva à variável, então permitir a reatribuição seria ambíguo e inseguro entre threads. Se você precisar de estado mutável compartilhado, use em vez disso um campo ou um invólucro como um array ou AtomicInteger.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR