Expressões Lambda em Java
Implementações inline concisas de interfaces funcionais em Java com expressões lambda: (params) -> corpo.
Uma expressão lambda é a sintaxe concisa que o Java 8 adicionou para "uma instância de uma interface que possui exatamente um método abstrato". Antes disso, você escrevia isso como uma classe anônima. Depois, você escreve como uma lista de parâmetros, uma seta e um corpo:
Runnable r = () -> System.out.println("hi");
Comparator<String> byLen = (a, b) -> a.length() - b.length();
Function<String, Integer> length = s -> s.length();Não há nenhum tipo de valor novo aqui — r, byLen e length ainda são referências a objetos, e em tempo de execução cada um contém uma instância de uma classe que implementa a interface à esquerda. O que há de novo é que o código que diz "crie um para mim" é curto o suficiente para caber no local da chamada, o que desbloqueia todos os outros idiomas funcionais na parte: predicados de filtro, construtores de comparador, manipuladores de eventos, pipelines de stream.
As formas de sintaxe
Uma lambda tem três partes: lista de parâmetros, seta -> e corpo. Cada parte tem abreviação:
// Zero parameters: empty parens are required
Runnable r = () -> System.out.println("tick");
// One parameter: parens optional (idiomatic to omit them)
Function<String, Integer> len = s -> s.length();
Function<String, Integer> len2 = (s) -> s.length(); // same thing
// Two or more: parens required
Comparator<String> cmp = (a, b) -> a.length() - b.length();
// Explicit types: rare but legal
BinaryOperator<Integer> add = (Integer a, Integer b) -> a + b;
// Expression body: the value of the expression is the return value
Predicate<Integer> positive = n -> n > 0;
// Block body: explicit `return` required if the interface method returns a value
Function<Integer, String> describe = n -> {
if (n == 0) return "zero";
if (n < 0) return "negative";
return "positive";
};Três regras conectam esses elementos:
- Os tipos de parâmetros geralmente são inferidos a partir do tipo alvo (a interface declarada no local da chamada). Escreva-os somente quando o compilador não conseguir escolher um ou quando auxiliarem na legibilidade.
- O corpo de expressão retorna seu valor implicitamente. Sem
return, sem ponto e vírgula. A expressão é o resultado. - O corpo de bloco precisa de
returnquando o método da interface tem um tipo de retorno. Esquecê-lo é um erro de compilação, não umnullsilencioso.
Tipagem alvo — onde lambdas podem aparecer
Uma lambda não tem tipo intrínseco. O compilador determina seu tipo a partir do alvo — o contexto onde é utilizada:
Runnable r1 = () -> doWork(); // target: Runnable
Callable<Integer> c1 = () -> 42; // target: Callable<Integer>
Supplier<Integer> s1 = () -> 42; // target: Supplier<Integer>() -> 42 é a mesma fonte nos três casos, mas compila para três instâncias de interface diferentes. Por isso uma lambda não pode ser atribuída a Object diretamente — Object o = () -> 42; é ambíguo e o compilador recusa. Faça um cast para desambiguar: Object o = (Supplier<Integer>) () -> 42;.
Alvos que você verá com mais frequência:
- Um parâmetro de método tipado como interface funcional:
list.removeIf(s -> s.isEmpty()). - Um campo ou variável local de tipo de interface funcional:
Predicate<String> empty = String::isEmpty;. - Um tipo de retorno:
public Supplier<Date> now() { return Date::new; }.
Se não há alvo, não pode haver lambda. var f = s -> s.length(); não compila — var não pode inferir um tipo alvo.
Captura de variável: "efetivamente final"
Uma lambda pode ler variáveis locais do método que a envolve, mas somente se essas variáveis forem efetivamente finais — nunca reatribuídas após seu valor inicial:
int multiplier = 3;
IntFunction<Integer> scale = n -> n * multiplier; // OK — `multiplier` never reassigned
multiplier = 4; // <-- this line would make the lambda not compileA regra é a mesma que as classes internas anônimas sempre tiveram, e o motivo é o mesmo: uma lambda pode sobreviver ao método em que foi definida (você pode armazená-la em um campo ou passá-la para outra thread), e Java não tem closures que capturam a variável — ela captura o valor no momento da construção. Permitir a reatribuição criaria uma ilusão confusa.
Campos são uma história diferente. Uma lambda pode ler e modificar campos de instância e estáticos livremente:
class Counter {
private int n = 0;
Runnable inc = () -> n++; // legal — `n` is a field, not a local
}Isso é uma fonte frequente de bugs em código de stream — uma lambda que modifica um campo compartilhado parece inocente, mas entra em corrida consigo mesma quando o stream se torna paralelo. Lambdas puras são mais seguras.
this, return e break dentro de uma lambda
Uma lambda não é um novo escopo para this. Dentro de uma lambda, this refere-se à instância que a envolve — o mesmo que o código ao redor:
class Greeter {
String prefix = "Hello, ";
Function<String, String> greet = name -> this.prefix + name; // `this` is the Greeter
}Essa é uma das maiores diferenças práticas em relação às classes anônimas, onde this se referia à própria instância anônima.
return dentro de uma lambda retorna da lambda, não do método que a envolve. break e continue não funcionam em uma lambda — eles pertencem ao loop ao qual se destinam, e o corpo da lambda não faz parte do loop ao redor.
Lambda versus classe anônima — quando cada uma se aplica
Para interfaces funcionais, lambdas são quase sempre mais curtas e claras. Elas geram bytecode ligeiramente diferente (invokedynamic) e não criam um novo arquivo de classe por local de uso, então geralmente são mais leves em tempo de execução também.
Use uma classe anônima quando:
- A interface tiver mais de um método abstrato (não é funcional).
- Você precisar de um campo local ao método (
int seen = 0;acessível entre chamadas). - Você precisar que
thisse refira à instância que está criando, não à instância que a envolve. - Você precisar sobrescrever um método padrão para especializar seu comportamento.
Em todos os outros casos, a lambda vence.
Um exemplo elaborado: captura, tipagem alvo e os quatro locais de chamada
O programa abaixo demonstra os quatro lugares mais comuns onde uma lambda aparece — forEach de coleção, removeIf, sort e filter de stream — junto com as regras de captura e tipagem alvo.
O que aprender com a execução:
() -> \"hi\"funcionou tanto comoCallable<String>quanto comoSupplier<String>— mesma fonte, tipos alvo diferentes, instâncias de interface diferentes. É por isso que uma lambda não tem tipo até que o contexto forneça um.times = n -> n * factorcapturoufactorpor valor. O compilador aceitou porquefactornunca foi reatribuído. Descomentarfactor = 11tornariafactoruma variável não efetivamente final e quebraria a compilação da lambda.forEach,removeIfesortrecebem uma interface funcional diferente (Consumer,Predicate,Comparator), e a forma da lambda — número de parâmetros, presença de um retorno — correspondeu ao único método abstrato de cada interface. O compilador faz a correspondência por tipagem alvo.- A lambda
describecom corpo de bloco precisou de instruçõesreturnexplícitas porque seu alvo (Function<Integer, String>) tem um tipo de retorno nãovoid. As lambdas com corpo de expressão acima dela retornaram sua expressão implicitamente.
O que vem a seguir
Você conhece a sintaxe e as regras de captura. A próxima pergunta é: a qual interface, exatamente, uma lambda está compilando? Interfaces Funcionais em Java apresenta a regra SAM (single-abstract-method), a anotação @FunctionalInterface e como escrever sua própria interface funcional para casos que a biblioteca padrão não cobre.