Dominando Expressões Lambda em Java: Do Básico à Stream API
Dominando Expressões Lambda em Java
Explore as expressões lambda do Java, interfaces funcionais, generics e a Stream API com exemplos claros.
O Java 8 introduziu expressões lambda para aumentar o poder expressivo da linguagem, permitindo que desenvolvedores escrevam código mais conciso e legível.
Antes de mergulhar em lambdas, é essencial entender interfaces funcionais, a espinha dorsal da implementação de lambda.
O que é uma Interface Funcional?
Uma interface funcional é uma interface Java que contém exatamente um método abstrato. Este único método define o propósito pretendido da interface.
A interface Runnable de java.lang é um exemplo clássico, expondo apenas o método run().
Exemplo 1: Definindo uma Interface Funcional
import java.lang.FunctionalInterface;
@FunctionalInterface
public interface MyInterface {
// Único método abstrato
double getValue();
}
Aqui, MyInterface é anotada com @FunctionalInterface, o que obriga o compilador a impor o contrato de interface funcional, prevenindo adição acidental de múltiplos métodos abstratos. A anotação é opcional mas altamente recomendada para clareza e segurança.
No Java 7, interfaces funcionais eram chamadas de Single Abstract Methods (SAMs) e eram tipicamente implementadas usando classes anônimas.
Exemplo 2: Implementando um SAM com Classe Anônima
public class FunctionInterfaceTest {
public static void main(String[] args) {
// Classe anônima implementando Runnable
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Acabei de implementar a Interface Funcional Runnable.");
}
}).start();
}
}
Saída:
Acabei de implementar a Interface Funcional Runnable.
Embora classes anônimas reduzam boilerplate comparado a implementações tradicionais, a sintaxe permanece verbosa. Expressões lambda eliminam a necessidade de nomes de métodos explícitos ao passar interfaces funcionais como argumentos.
Introdução a Expressões Lambda
Uma expressão lambda é essencialmente um método anônimo que fornece uma implementação concisa do método abstrato de uma interface funcional. Ela não executa independentemente; em vez disso, fornece o comportamento para o método da interface.
Como Definir uma Expressão Lambda em Java
A sintaxe geral é:
(lista de parâmetros) -> corpo do lambda
O operador seta (->) separa a lista de parâmetros do corpo. Abaixo estão exemplos concretos.
Suponha que temos um método retornando Pi:
double getPiValue() { return 3.1415; }
Usando lambda:
() -> 3.1415
Sem parâmetros, o lado esquerdo da seta está vazio, e o lado direito é o corpo da expressão que retorna o valor.
Tipos de Corpos de Lambda
Corpos de lambda em Java vêm em duas formas:
1. Expression Body – contém uma única expressão.
() -> System.out.println("Lambdas são ótimas");
2. Block Body – encapsulado em chaves, permitindo múltiplas declarações e return opcional.
() -> { double pi = 3.1415; return pi; };
Nota: Expression bodies não requerem return explícito, enquanto block bodies exigem se um valor for retornado.
Exemplo 3: Expressão Lambda na Prática
Vamos escrever um programa Java que retorna Pi usando uma expressão lambda.
import java.lang.FunctionalInterface;
@FunctionalInterface
interface MyInterface {
double getPiValue();
}
public class Main {
public static void main(String[] args) {
MyInterface ref = () -> 3.1415;
System.out.println("Valor de Pi = " + ref.getPiValue());
}
}
Saída:
Valor de Pi = 3.1415
Principais conclusões:
MyInterfacedefine um único método abstrato.- Uma referência a
MyInterfaceé declarada mas não instanciada diretamente. - A expressão lambda fornece a implementação para
getPiValue(). - Invocar
ref.getPiValue()dispara o corpo do lambda.
Expressões Lambda com Parâmetros
Como métodos, lambdas podem aceitar parâmetros. Exemplo:
(n) -> (n % 2) == 0
Este lambda verifica se o inteiro de entrada é par.
Exemplo 4: Invertendo uma String com Lambda Parametrizado
@FunctionalInterface
interface MyInterface {
String reverse(String s);
}
public class Main {
public static void main(String[] args) {
MyInterface ref = (str) -> {
String result = "";
for (int i = str.length() - 1; i >= 0; i--) {
result += str.charAt(i);
}
return result;
};
System.out.println("Lambda invertido = " + ref.reverse("Lambda"));
}
}
Saída:
Lambda invertido = adbmaL
Interface Funcional Genérica
Interfaces funcionais podem ser genéricas, permitindo operar em qualquer tipo de dado. Por exemplo:
@FunctionalInterface
interface MyInterface<T> {
T func(T t);
}
Exemplo 5: Lambdas Genéricas para Strings e Inteiros
public class Main {
public static void main(String[] args) {
// Inversão de String
MyInterface<String> reverse = (str) -> {
String result = "";
for (int i = str.length() - 1; i >= 0; i--) {
result += str.charAt(i);
}
return result;
};
System.out.println("Lambda invertido = " + reverse.func("Lambda"));
// Fatorial de Inteiro
MyInterface<Integer> factorial = (n) -> {
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
};
System.out.println("fatorial de 5 = " + factorial.func(5));
}
}
Saída:
Lambda invertido = adbmaL fatorial de 5 = 120
Expressões Lambda e a Stream API
O pacote java.util.stream, introduzido no JDK 8, capacita desenvolvedores a realizar operações de estilo funcional em coleções: filtro, map, reduce, sort e mais.
Considere uma lista de nomes de lugares, cada um prefixado por seu país. Usando streams e lambdas, podemos extrair e manipular dados eficientemente.
Exemplo 6: Processamento de Stream com Lambdas
import java.util.ArrayList;
import java.util.List;
public class StreamMain {
static List<String> places = new ArrayList<>();
public static List<String> getPlaces() {
places.add("Nepal, Kathmandu");
places.add("Nepal, Pokhara");
places.add("India, Delhi");
places.add("USA, New York");
places.add("Africa, Nigeria");
return places;
}
public static void main(String[] args) {
List<String> myPlaces = getPlaces();
System.out.println("Lugares do Nepal:");
myPlaces.stream()
.filter(p -> p.startsWith("Nepal"))
.map(p -> p.toUpperCase())
.sorted()
.forEach(p -> System.out.println(p));
}
}
Saída:
Lugares do Nepal: NEPAL, KATHMANDU NEPAL, POKHARA
Aqui, a cadeia de operações de stream — filter, map, sorted, forEach — cada uma aceita um lambda, reduzindo drasticamente boilerplate comparado a soluções pré-Java-8.
Java
- Dominando Interfaces em C#: Definição, Implementação e Casos Práticos
- Dominando Operadores Java: Tipos, Sintaxe e Exemplos Práticos
- Entendendo Expressões, Instruções e Blocos em Java
- Dominando Interfaces em Java: Conceitos, Implementação e Boas Práticas
- Dominando Try-With-Resources em Java: Gerenciamento Automático de Recursos Explicado
- Anotações em Java Explicadas: Tipos, Posicionamento e Exemplos Práticos
- Interface vs Classe Abstrata em Java: Como Escolher a Abstração Certa
- Expressões Regulares em Java: Dominando Correspondência de Padrões com java.util.regex
- Entendendo Interfaces em Java: Definições, Usos e Boas Práticas
- Java 9 Métodos Privados em Interfaces: Aumentando Encapsulamento e Reuso de Código