1. Passar comportamento em vez de dados
Por que passar uma função como parâmetro?
Imagine que você tem um método que ordena uma lista. Mas como ele vai saber exatamente como ordenar? Por ordem alfabética? Pelo tamanho? Pela data de nascimento? Poderíamos escrever um método separado para cada caso, mas isso rapidamente viraria uma cópia e cola infernal.
Em vez disso, o Java permite passar o comportamento (isto é, uma função ou lambda) que define como ordenar, filtrar, transformar e assim por diante. Isso torna o código flexível e reutilizável.
Exemplo: ordenação com Comparator
List<String> names = List.of("Anna", "Boris", "Vika");
// Passamos o comportamento: como comparar os elementos (pelo comprimento da string)
names.stream()
.sorted((a, b) -> a.length() - b.length())
.forEach(System.out::println);
Exemplo: filtragem com Predicate
List<String> names = List.of("Anna", "Boris", "Vika");
// Passamos o comportamento: quem manter (nome com mais de 4 caracteres)
names.stream()
.filter(name -> name.length() > 4)
.forEach(System.out::println);
Em ambos os casos, você não “trancou” rigidamente a lógica dentro do método — deu a ele um pedaço de comportamento que é aplicado a cada elemento.
Benefício: menos duplicação, mais flexibilidade
Em vez de dezenas de métodos com código parecido, mas lógica diferente, você escreve um único método genérico que recebe “o que fazer” na forma de função. Isso economiza tempo, reduz erros e torna o código mais fácil de testar.
2. Sintaxe de passar funções
Expressões lambda como argumentos
A forma mais comum é escrever a lambda diretamente no local da chamada, com a seta ->:
list.forEach(item -> System.out.println(item));
Ou até mais curto, se já existir um método apropriado — referência ao método (::):
list.forEach(System.out::println);
Referências a métodos (method references)
Se você já tem um método adequado, é possível passá-lo como referência :::
// Método comum
public static boolean isLongName(String name) {
return name.length() > 4;
}
// Passando a referência ao método
names.stream()
.filter(MyClass::isLongName)
.forEach(System.out::println);
Isso funciona se a assinatura do método coincide com a esperada pelo interface funcional (Predicate<T>, Function<T, R>, Comparator<T> etc.).
3. Exemplos da biblioteca padrão
Collections.sort e Comparator
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));
// Ordenação pelo comprimento do nome
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [Vika, Anna, Boris]
Stream.filter e Predicate
List<String> names = List.of("Anna", "Boris", "Vika");
// Manter apenas nomes que começam com 'V'
names.stream()
.filter(name -> name.startsWith("V"))
.forEach(System.out::println); // Vika
Stream.map e Function
List<String> names = List.of("Anna", "Boris", "Vika");
// Converter os nomes para maiúsculas
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
Optional.ifPresent e Consumer
Optional<String> opt = Optional.of("Olá!");
// Se houver valor, imprima-o
opt.ifPresent(s -> System.out.println("Na string: " + s));
4. Prática: escrevendo métodos próprios com funções-parâmetro
Hora de colocar o conhecimento em prática! Vamos escrever alguns métodos que recebem uma função como parâmetro e a utilizam internamente.
Exemplo 1: Método para processar elementos de uma lista (forEach à sua maneira)
Suponha que temos uma lista de usuários (User). Queremos executar alguma ação para cada usuário — por exemplo, exibir o nome, enviar e-mail, creditar bônus etc. Em vez de “trancar” a ação no código, vamos passá-la como parâmetro — Consumer<User>!
import java.util.List;
import java.util.function.Consumer;
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
}
public class UserProcessor {
// Método que recebe Consumer<User>
public static void processUsers(List<User> users, Consumer<User> action) {
for (User user : users) {
action.accept(user);
}
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 30),
new User("Vika", 22)
);
// Exibir os nomes de todos os usuários
processUsers(users, user -> System.out.println("Nome: " + user.name));
// Creditar um bônus (neste exemplo, apenas exibir)
processUsers(users, user -> System.out.println(user.name + " recebeu um bônus!"));
}
}
Exemplo 2: Método para filtrar elementos (Predicate)
Vamos escrever um método que retorna apenas os usuários que atendem a uma condição (por exemplo, apenas maiores de idade):
import java.util.List;
import java.util.ArrayList;
import java.util.function.Predicate;
public class UserFilter {
public static List<User> filterUsers(List<User> users, Predicate<User> condition) {
List<User> result = new ArrayList<>();
for (User user : users) {
if (condition.test(user)) {
result.add(user);
}
}
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// Filtrar apenas maiores de idade
List<User> adults = filterUsers(users, user -> user.age >= 18);
adults.forEach(user -> System.out.println(user.name)); // Anna, Vika
}
}
Exemplo 3: Método transformador (Function)
Agora, um método que, a partir de uma lista de usuários, cria uma lista com seus nomes (ou quaisquer outras transformações):
import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;
public class UserMapper {
public static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
List<R> result = new ArrayList<>();
for (User user : users) {
result.add(mapper.apply(user));
}
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// Obter a lista de nomes
List<String> names = mapUsers(users, user -> user.name);
System.out.println(names); // [Anna, Boris, Vika]
// Obter a lista de idades
List<Integer> ages = mapUsers(users, user -> user.age);
System.out.println(ages); // [25, 17, 22]
}
}
Exemplo 4: Método gerador (Supplier)
Às vezes é preciso obter um valor “sob demanda” — por exemplo, gerar um número aleatório, criar um objeto, obter a hora atual. Para isso serve a interface Supplier<T>.
import java.util.function.Supplier;
public class ValueGenerator {
public static int getValue(Supplier<Integer> supplier) {
return supplier.get();
}
public static void main(String[] args) {
// Obter um número aleatório
int random = getValue(() -> (int)(Math.random() * 100));
System.out.println("Número aleatório: " + random);
// Obter a hora atual em milissegundos
long time = getValue(System::currentTimeMillis);
System.out.println("Tempo: " + time);
}
}
5. Aplicação única: unindo os exemplos
Vamos imaginar que estamos desenvolvendo um aplicativo simples “Lista de usuários”. Já sabemos:
- Filtrar usuários por condição (por exemplo, apenas maiores de idade);
- Transformar usuários em outra coisa (nomes, e-mail, idade);
- Executar ações para cada usuário (impressão, crédito de bônus);
- Gerar valores sob demanda (por exemplo, criar novos usuários).
Agora, usando todos esses métodos, podemos construir cenários flexíveis de processamento de dados, sem reescrever o código a cada nova tarefa.
import java.util.*;
import java.util.function.*;
public class UserApp {
static class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// Método universal para processar usuários
static void processUsers(List<User> users, Consumer<User> action) {
for (User user : users) action.accept(user);
}
// Filtro universal
static List<User> filterUsers(List<User> users, Predicate<User> condition) {
List<User> result = new ArrayList<>();
for (User user : users) if (condition.test(user)) result.add(user);
return result;
}
// Transformador universal
static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
List<R> result = new ArrayList<>();
for (User user : users) result.add(mapper.apply(user));
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// 1. Exibir todos os usuários
processUsers(users, user -> System.out.println("Usuário: " + user));
// 2. Encontrar apenas adultos
List<User> adults = filterUsers(users, user -> user.age >= 18);
System.out.println("Maiores de idade: " + adults);
// 3. Obter os nomes dos adultos
List<String> adultNames = mapUsers(adults, user -> user.name);
System.out.println("Nomes dos adultos: " + adultNames);
// 4. Creditar bônus aos adultos
processUsers(adults, user -> System.out.println(user.name + " recebeu um bônus!"));
}
}
Por que isso é melhor do que a abordagem “comum”?
Esse modo oferece flexibilidade: os mesmos métodos podem ser usados em cenários totalmente diferentes, apenas passando outras funções. O código não cresce em infinitas variantes “filtrar assim”, “imprimir assado” — temos ferramentas gerais que funcionam em qualquer lugar. Com isso, ele fica mais compacto, claro e menos propenso a erros. Além disso, esse estilo se integra muito bem com a Stream API moderna, então, ao migrar para streams, você não precisará aprender tudo de novo — a abordagem é a mesma.
6. Erros típicos ao passar funções como parâmetros
Erro nº 1: A assinatura não corresponde à interface esperada.
Se o método recebe Predicate<User>, mas você passa uma lambda que retorna não um boolean, e sim, por exemplo, uma String, o compilador logo vai dizer “ai-ai-ai!” e não deixará compilar o projeto. Verifique se o valor de retorno e os parâmetros coincidem com os esperados.
Erro nº 2: A lambda usa variáveis que podem mudar.
No Java, expressões lambda só podem usar variáveis final ou efetivamente finais do contexto externo. Se você tentar alterar tal variável dentro da lambda — haverá erro de compilação.
Erro nº 3: Confusão entre interfaces.
Às vezes você quer passar, por exemplo, um Consumer<T>, mas acaba escrevendo uma função que retorna algo (por exemplo, uma Function<T, R>). Verifique se sua lambda retorna exatamente o que é necessário (ou nada — no caso de Consumer).
Erro nº 4: Lambdas muito complexas diretamente nos parâmetros.
Se a lambda ocupa mais de uma ou duas linhas, é melhor extraí-la para uma variável ou método separado. Caso contrário, o código ficará ilegível e só você vai entendê-lo (e olhe lá).
GO TO FULL VERSION