Programação Java
Lambdas, method references e interfaces funcionais em Java
Intermediário9 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
As lambdas transformaram o Java em uma linguagem em que comportamento circula com a mesma facilidade que dados. Este guia explica o que uma lambda realmente é — uma instância de uma interface funcional —, como o compilador infere o seu tipo, as interfaces prontas de java.util.function, os quatro tipos de method reference, as regras de captura de variáveis e como as lambdas são compiladas. Você vai conseguir escrever, ler e projetar APIs baseadas em lambdas com segurança.
O que você vai aprender
- Explicar como uma expressão lambda é tipada em relação a uma interface funcional
- Escolher a interface certa de java.util.function, incluindo as especializações para primitivos
- Converter entre lambdas e as quatro formas de method reference
- Aplicar a regra effectively final e entender por que uma lambda não pode alterar variáveis locais capturadas
- Compor funções, predicados e comparadores, e projetar métodos que recebem comportamento
O Java 8 trouxe as expressões lambda e, com elas, uma mudança na forma de escrever código Java: comportamento passou a ser um argumento de primeira classe. Ordenação, filtros, políticas de retry, event handlers e corpos de threads viraram uma linha só em vez de uma classe anônima. A sintaxe é fácil; o que confunde é o modelo de tipos (uma lambda não tem tipo próprio), as regras de captura e o catálogo cada vez maior de interfaces em java.util.function. Este guia cobre esses fundamentos, dos quais dependem a API de streams e boa parte do Java moderno. O código tem como alvo o Java 21.
O que é uma lambda
Uma expressão lambda é uma forma compacta de criar uma instância de uma interface funcional — uma interface com exatamente um método abstrato. Os parâmetros e o corpo da lambda passam a ser a implementação desse método.
// Before: an anonymous class
Comparator<String> byLength = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
};
// After: a lambda
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
O formato é parameters -> body. Os tipos dos parâmetros costumam ser inferidos; um único parâmetro dispensa parênteses; um corpo com uma só expressão dispensa chaves e return; um corpo em bloco usa os dois. Desde o Java 11, var é permitido nos tipos dos parâmetros, principalmente para que você possa anotá-los.
Runnable hello = () -> System.out.println("hi"); // no parameters
Function<String, Integer> len = s -> s.length(); // one parameter, expression body
BiFunction<Integer, Integer, Integer> add = (Integer a, Integer b) -> { // explicit types, block body
int sum = a + b;
return sum;
};
Tipo-alvo
Uma lambda não tem tipo intrínseco. (a, b) -> a + b poderia ser um BinaryOperator<Integer>, uma BiFunction<String, String, String> ou um Comparator<Integer> (um péssimo). O compilador determina o tipo-alvo a partir do contexto — a variável que recebe a atribuição, o parâmetro do método, o tipo de retorno, um cast — e verifica a lambda contra o único método abstrato dessa interface. É por isso que não dá para escrever var f = x -> x + 1; (não há alvo) e por isso métodos sobrecarregados que aceitam interfaces funcionais diferentes às vezes precisam de um cast ou de tipos de parâmetro explícitos para desfazer a ambiguidade.
Qualquer interface com um único método abstrato serve, inclusive as suas e as antigas, como Runnable, Callable, Comparator e ActionListener. Marque as suas com @FunctionalInterface: a anotação não é obrigatória, mas faz o compilador rejeitar um segundo método abstrato que quebraria silenciosamente todas as lambdas que têm a interface como alvo.
O catálogo de java.util.function
Em vez de cada biblioteca inventar as suas próprias interfaces de método único, o JDK oferece um conjunto padrão. Aprenda as seis formas básicas e o esquema de nomes, e você decifra todo o resto.
| Interface | Método | Significado |
|---|---|---|
Supplier<T> | T get() | Produz um valor, sem entrada |
Consumer<T> | void accept(T t) | Usa um valor, não retorna nada |
Function<T, R> | R apply(T t) | Transforma T em R |
Predicate<T> | boolean test(T t) | Decisão de sim ou não |
UnaryOperator<T> | T apply(T t) | Função de T para T |
BinaryOperator<T> | T apply(T a, T b) | Combina dois T em um único T |
O prefixo Bi dobra as entradas (BiFunction<T, U, R>, BiConsumer<T, U>, BiPredicate<T, U>). Existem especializações para primitivos que evitam o boxing: IntPredicate, IntFunction<R> (recebe int), ToIntFunction<T> (retorna int), IntUnaryOperator, IntBinaryOperator, IntSupplier, ObjIntConsumer<T>, com variantes Long e Double. É por isso que um stream chama mapToInt(ToIntFunction).
Supplier<List<String>> newList = ArrayList::new;
Consumer<String> print = System.out::println;
Function<String, Integer> parse = Integer::parseInt;
Predicate<String> isBlank = String::isBlank;
UnaryOperator<String> trim = String::strip;
BinaryOperator<Integer> max = Integer::max;
IntBinaryOperator gcd = (a, b) -> { while (b != 0) { int t = b; b = a % b; a = t; } return a; };
Recorra a essas interfaces antes de definir a sua. Uma interface funcional própria se justifica quando o nome carrega significado de domínio (PricingRule, RetryPolicy) ou quando o método precisa declarar uma checked exception, o que nenhuma das interfaces padrão faz.
Method references
Uma method reference nomeia um método existente onde uma lambda apenas o chamaria. Há quatro formas:
| Forma | Sintaxe | Lambda equivalente |
|---|---|---|
| Método estático | Integer::parseInt | s -> Integer.parseInt(s) |
| Método de instância de um objeto específico | System.out::println | x -> System.out.println(x) |
| Método de instância de um objeto qualquer de um tipo | String::toUpperCase | s -> s.toUpperCase() |
| Construtor | ArrayList::new | () -> new ArrayList<>() |
A terceira forma é a que parece estranha no começo: String::compareToIgnoreCase usado como Comparator<String> significa “chame compareToIgnoreCase no primeiro argumento, passando o segundo”. O primeiro parâmetro vira o receptor.
As referências vinculadas (a segunda forma) avaliam a expressão do receptor uma única vez, no momento em que a referência é criada. this::handle ou logger::info capturam o objeto atual; se o objeto mudar depois, a referência continua apontando para o antigo.
Method references nem sempre são mais claras. Person::getName é melhor que p -> p.getName(); mas entre (a, b) -> a.compareTo(b) e String::compareTo é questão de gosto, e uma referência a um método sobrecarregado pode deixar o compilador sem conseguir escolher a sobrecarga. Na dúvida, escreva a lambda.
Captura de variáveis e a regra effectively final
O corpo de uma lambda pode ler variáveis locais, parâmetros, campos e this do escopo externo. Variáveis locais e parâmetros, porém, precisam ser effectively final: atribuídos exatamente uma vez, quer você escreva a palavra-chave final ou não. O compilador rejeita uma lambda que escreve em uma variável local, e também rejeita uma lambda que lê uma variável local alterada em qualquer ponto após a inicialização.
int base = 10;
Function<Integer, Integer> addBase = x -> x + base; // fine: base is effectively final
int counter = 0;
list.forEach(item -> counter++); // compile error: counter is modified
A regra existe porque a lambda recebe uma cópia do valor. A variável local vive no stack frame do método externo; a lambda pode ser executada depois que esse frame deixou de existir, ou em outra thread. Compartilhar uma posição mutável exigiria que a JVM movesse a variável para o heap e sincronizasse o acesso, e os projetistas da linguagem preferiram proibir isso a esconder o custo.
Quando você realmente precisa acumular, as opções, em ordem de preferência, são: expressar o cálculo como uma redução ou um collector, para que não exista estado compartilhado; usar um AtomicInteger ou LongAdder quando a lambda é um callback que precisa contar; ou, em último caso, um array de um elemento (int[] counter = {0}), que compila porque a referência é final mesmo que o conteúdo mude — e que vira uma data race se a lambda rodar concorrentemente.
Campos não estão sujeitos à regra. this.counter++ dentro de uma lambda compila, porque o que é capturado é this, e o campo é acessado por meio dele. Isso é conveniente em um handler de interface gráfica single-thread e é um bug em um parallel stream.
this em lambdas versus classes anônimas
Dentro de uma lambda, this é a instância externa, exatamente como no método que a contém. Dentro de uma classe anônima, this é o próprio objeto anônimo. Essa diferença é o motivo mais visível pelo qual uma lambda não é açúcar sintático para uma classe anônima: converter uma na outra pode mudar aquilo a que this.name se refere.
Composição
As interfaces funcionais vêm com métodos default para combinar instâncias, de modo que peças pequenas podem ser montadas sem escrever novas lambdas.
Predicate<String> notBlank = Predicate.not(String::isBlank);
Predicate<String> shortEnough = s -> s.length() <= 80;
Predicate<String> validTitle = notBlank.and(shortEnough);
Function<String, String> normalise = String::strip;
Function<String, String> pipeline = normalise.andThen(String::toLowerCase); // strip, then lower-case
Function<String, Integer> lengthAfter = pipeline.andThen(String::length);
Comparator<Employee> ordering = Comparator.comparing(Employee::department)
.thenComparing(Employee::salary, Comparator.reverseOrder())
.thenComparing(Employee::name, String.CASE_INSENSITIVE_ORDER);
UnaryOperator<String> identity = UnaryOperator.identity();
andThen aplica primeiro o receptor e depois o argumento; compose faz o contrário. Predicate oferece and, or, negate e o método estático not. Comparator é a mais rica: comparing, comparingInt, thenComparing, reversed, nullsFirst, nullsLast e naturalOrder. Compor comparadores dessa forma substitui métodos compare escritos à mão, que eram uma fonte garantida de erros de sinal.
Projetando APIs em torno de lambdas
O ganho das lambdas não está só em chamadas mais curtas; está em permitir que bibliotecas sejam construídas em torno da parametrização de comportamento: um método que recebe uma estratégia como argumento em vez de um flag. Compare um método com três parâmetros booleanos com outro que recebe um Predicate<Order> — o segundo é menor, mais genérico e dá para testá-lo sem o código que o chama.
public final class Retry {
public static <T> T withRetry(int attempts, Predicate<Exception> retryable, Supplier<T> action) {
Exception last = null;
for (int i = 0; i < attempts; i++) {
try {
return action.get();
} catch (Exception e) {
if (!retryable.test(e)) throw e instanceof RuntimeException r ? r : new RuntimeException(e);
last = e;
}
}
throw new IllegalStateException("Gave up after " + attempts + " attempts", last);
}
}
String body = Retry.withRetry(3, e -> e instanceof IOException, () -> client.fetch("/health"));
Diretrizes para esse tipo de API:
- Aceite a interface padrão mais genérica que servir:
Supplier<T>em vez de umLoader<T>próprio;Function<? super T, ? extends R>com wildcards, como explica o guia de generics, para que quem chama possa passar mais do que os tipos exatos. - Prefira uma especialização para primitivos quando o valor for primitivo;
IntPredicateevita boxing em um loop crítico. - Deixe a lambda ser pura sempre que possível, e documente quando a API a chama mais de uma vez, de forma lazy ou em outra thread.
- Considere uma pequena interface própria quando ela melhorar a legibilidade da chamada ou precisar de uma checked exception.
A única limitação real são as checked exceptions. Um Supplier<T> não pode lançar IOException, então uma lambda que chama Files.readString precisa capturá-la e encapsulá-la. Ou você declara o seu próprio ThrowingSupplier<T, E extends Exception> para APIs em que isso é esperado, ou encapsula em uma UncheckedIOException no ponto de chamada.
Como as lambdas são compiladas
Entender a implementação explica o comportamento. O compilador transforma o corpo de uma lambda em um método sintético privado da classe externa e emite uma instrução invokedynamic no ponto em que a lambda aparece. Na primeira execução, a LambdaMetafactory da JVM gera uma pequena classe oculta que implementa a interface funcional e liga o call site a ela. Consequências:
- Nenhum arquivo .class extra no seu JAR, ao contrário das classes anônimas, que geram cada uma um
Outer$1.class. - Lambdas sem captura (sem referência a variáveis locais nem a
this) são criadas uma vez e ficam em cache; cada avaliação retorna a mesma instância. Lambdas com captura alocam uma nova instância a cada avaliação, com cópias dos valores capturados. - A identidade não é especificada. Nunca compare lambdas com
==nem as use como chaves de map; duas avaliações do mesmo código-fonte podem ou não resultar no mesmo objeto. - A serialização só funciona se a interface-alvo for
Serializable, e é frágil entre recompilações. Evite.
Em desempenho, uma chamada de lambda após o warm-up do JIT é comparável a uma chamada de interface e normalmente sofre inlining. Os custos que aparecem nos profiles vêm do boxing (use as interfaces para primitivos), das alocações por captura em loops críticos e de call sites megamórficos, em que uma mesma linha de código despacha para muitas lambdas diferentes.
Erros comuns
Alterar estado capturado. O truque da posição de array e a alteração de campos compilam, mas geram race conditions em execução paralela. Use redução ou collector.
Corpos de lambda longos. Um bloco de dez linhas em um pipeline de streams esconde o formato do pipeline. Extraia um método com nome e referencie-o.
Sobrecargas que recebem interfaces funcionais diferentes. submit(Runnable) e submit(Callable<T>) em ExecutorService já causam ambiguidade com () -> doWork(). Evite projetar sobrecargas assim; se não tiver jeito, dê nomes diferentes a elas.
Ignorar o descompasso com checked exceptions. Envolver cada chamada em try/catch dentro da lambda é sinal de que vale introduzir uma interface que lança exceções ou reestruturar o código.
Supor execução imediata. Passar uma lambda para um método não a executa. Se o método chamado a guardar e chamar depois, os objetos capturados continuam vivos até lá, o que pode prender objetos grandes na memória.
Usar Optional.get em uma cadeia de lambdas. Componha com map/flatMap/orElse; veja o guia de Optional.
Lambdas de comparação com subtração. (a, b) -> a.value - b.value estoura com valores grandes. Use Integer.compare ou Comparator.comparingInt.
Principais pontos
- Uma lambda é uma instância de uma interface funcional; o seu tipo vem do contexto em que ela aparece, nunca da própria lambda.
java.util.functionfornece as formas padrão —Supplier,Consumer,Function,Predicate,UnaryOperator,BinaryOperator— além das variantesBie para primitivos. Use-as antes de escrever as suas.- Method references têm quatro formas; use-as quando a lambda apenas repassaria os seus argumentos.
- Variáveis locais capturadas precisam ser effectively final porque a lambda recebe uma cópia; campos e
thissão capturados por referência. - Componha comportamento com
andThen,compose,and,or,negatee os builders deComparatorem vez de escrever novas lambdas. - Lambdas são compiladas para
invokedynamicmais um método sintético, não para classes anônimas: sem arquivos .class, instâncias sem captura em cache,thisé o objeto externo e a identidade não é garantida.
Fontes e leituras complementares
Perguntas frequentes
- O que é uma interface funcional?
- Uma interface com exatamente um método abstrato. Métodos default e static não contam, nem os métodos que Object já fornece, como equals. Qualquer interface funcional pode ser o tipo-alvo de uma lambda ou de uma method reference. A anotação @FunctionalInterface é opcional, mas faz o compilador garantir a regra do método único.
- Por que as variáveis usadas em uma lambda precisam ser effectively final?
- Uma lambda captura o valor de uma variável local, não a variável em si: a variável local vive na pilha e pode já ter deixado de existir quando a lambda for executada, talvez em outra thread. Permitir escrita faria a cópia e o original divergirem silenciosamente. Se você precisa de estado mutável, use um campo, uma posição de array, um AtomicInteger ou, melhor ainda, reestruture o código com uma redução ou um collector.
- Uma lambda é só uma sintaxe abreviada para uma classe interna anônima?
- Não. Uma lambda é compilada para um método privado mais um call site invokedynamic que cria a instância da interface funcional sob demanda; nenhum arquivo .class é gerado em tempo de compilação, this se refere à instância externa e uma lambda sem captura normalmente é alocada uma única vez e reutilizada. Uma classe anônima é uma classe separada, com o seu próprio this e um objeto novo a cada avaliação.
- Quando usar uma method reference em vez de uma lambda?
- Quando a lambda não faria nada além de repassar os seus parâmetros para um único método existente: String::length em vez de s -> s.length(). Se for preciso adaptar os argumentos de alguma forma, ou se a referência ficar ambígua ou obscura, escreva a lambda.