1. Conceito de composição de funções
Um pouco de teoria (sem chatice)
Na matemática, composição de funções é quando o resultado de uma função vira a entrada de outra. Se temos funções f e g, então a composição g(f(x)) significa: primeiro aplicamos f a x, depois passamos o resultado para g.
Em programação, a ideia é a mesma: queremos montar transformações complexas a partir de simples, para não escrever uma função gigante para tudo. Isso torna o código flexível, reutilizável e legível.
Imagine uma linha de produção de doces: primeiro a massa (f), depois o creme (g), depois o granulado (h). O processo inteiro é h(g(f(ingredientes))).
Por que a composição é importante?
- A composição permite montar o programa a partir de pequenas funções-“blocos”.
- Facilita a reutilização: um “bloco” pode ser usado em vários lugares sem duplicação.
- Flexibilidade: para alterar uma etapa, substituímos a função correspondente — o restante não é afetado.
- Legibilidade e testabilidade: funções pequenas são mais fáceis de ler, verificar e manter.
2. Métodos compose e andThen na interface Function
Interface Function: lembrete
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// Métodos para composição:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
}
- compose: primeiro é executada a função que você passa para compose e depois — a função atual.
- andThen: primeiro é executada a função atual e depois — a que foi passada para andThen.
Esquema visual
// Suponha que existam duas funções:
Function<String, Integer> parse = s -> Integer.parseInt(s);
Function<Integer, Integer> square = x -> x * x;
// compose: square.compose(parse) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25
// andThen: parse.andThen(square) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25
// Mas a ordem importa se os tipos forem diferentes!
Exemplo: converter string em número e depois ao quadrado
import java.util.function.Function;
public class ComposeAndThenDemo {
public static void main(String[] args) {
// Função: converte string em número
Function<String, Integer> parse = s -> Integer.parseInt(s);
// Função: eleva o número ao quadrado
Function<Integer, Integer> square = x -> x * x;
// Combinando: primeiro faz o parse, depois eleva ao quadrado
Function<String, Integer> parseThenSquare = parse.andThen(square);
System.out.println(parseThenSquare.apply("7")); // 49
// E se invertermos?
// square.compose(parse) — o mesmo resultado (para essas funções)
Function<String, Integer> squareOfParsed = square.compose(parse);
System.out.println(squareOfParsed.apply("8")); // 64
}
}
Quando a ordem importa?
Se os tipos das funções não coincidirem, a ordem se torna crítica. Por exemplo:
Function<String, String> addPrefix = s -> "User: " + s;
Function<String, Integer> length = s -> s.length();
Function<String, Integer> composed = addPrefix.andThen(length);
System.out.println(composed.apply("Alice")); // "User: Alice" -> 11
// E assim:
// length.andThen(addPrefix) — erro de compilação!
// length retorna Integer, e addPrefix aceita String.
Tabela: diferença entre compose e andThen
|
|
|
|
|---|---|---|---|
|
|
|
|
3. Composição de predicados e outras interfaces
Predicate<T>: and, or, negate
A interface funcional Predicate<T> é uma função que retorna boolean. Para combinar predicados existem métodos especiais:
- and: operador lógico E (&&)
- or: operador lógico OU (||)
- negate: operador lógico NÃO (!)
Exemplo: condições de filtragem complexas
Suponha que temos uma classe de usuário:
public class User {
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
Agora vamos escrever diferentes predicados:
import java.util.function.Predicate;
Predicate<User> isAdult = user -> user.age >= 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");
// Combinamos: adulto e nome começando com "A"
Predicate<User> adultAndA = isAdult.and(nameStartsWithA);
// Adulto ou nome começando com "A"
Predicate<User> adultOrA = isAdult.or(nameStartsWithA);
// Não adulto
Predicate<User> notAdult = isAdult.negate();
Agora podemos usar esses predicados na filtragem, por exemplo, com a API Stream:
import java.util.List;
import java.util.stream.Collectors;
List<User> users = List.of(
new User("Alice", 20),
new User("Bob", 17),
new User("Anna", 15),
new User("Mike", 22)
);
List<User> filtered = users.stream()
.filter(adultAndA)
.collect(Collectors.toList());
// Em filtered entrará apenas Alice (adulta e nome começando com "A")
Composição de Consumer, Function, Supplier
- Consumer<T>: o método andThen permite executar duas operações em sequência.
- Function<T, R>: a composição foi vista acima.
- Supplier<T>: não se combina diretamente, mas pode ser usado dentro de outras funções.
Exemplo: Consumer<T>.andThen
import java.util.function.Consumer;
Consumer<String> print = s -> System.out.println("Recebido: " + s);
Consumer<String> printUpper = s -> System.out.println("Em maiúsculas: " + s.toUpperCase());
Consumer<String> combined = print.andThen(printUpper);
combined.accept("hello");
// Irá imprimir:
// Recebido: hello
// Em maiúsculas: HELLO
4. Prática: cadeias de transformações e filtragens
Tarefa 1: Montar uma cadeia de transformações Function
Suponha que, no nosso aplicativo, armazenamos usuários como strings “Nome,Idade”, por exemplo “Alice,20”. É preciso:
- Converter a string em um objeto User
- Obter a idade
- Verificar se o usuário é maior de idade
import java.util.function.Function;
import java.util.function.Predicate;
Function<String, User> stringToUser = str -> {
String[] parts = str.split(",");
return new User(parts[0], Integer.parseInt(parts[1]));
};
Function<User, Integer> getAge = user -> user.age;
Predicate<Integer> isAdultAge = age -> age >= 18;
// Vamos combinar: string -> User -> idade -> predicado
Function<String, Integer> stringToAge = stringToUser.andThen(getAge);
String input = "Bob,19";
int age = stringToAge.apply(input);
System.out.println("Idade: " + age); // 19
System.out.println("Maior de idade? " + isAdultAge.test(age)); // true
Tarefa 2: Combinar vários Predicate para filtragem
Digamos que precisamos selecionar usuários que têm mais de 18 anos e cujo nome começa com "A" ou "M".
Predicate<User> isAdult = user -> user.age > 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");
Predicate<User> nameStartsWithM = user -> user.name.startsWith("M");
Predicate<User> filter = isAdult.and(nameStartsWithA.or(nameStartsWithM));
List<User> filtered = users.stream()
.filter(filter)
.collect(Collectors.toList());
Tarefa 3: Transformação em múltiplas etapas com Function
Suponha que precisamos: receber uma string, remover espaços nas extremidades, converter para maiúsculas e adicionar o prefixo "USER: ".
Function<String, String> trim = String::trim;
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> addPrefix = s -> "USER: " + s;
// Montamos a cadeia
Function<String, String> pipeline = trim.andThen(toUpper).andThen(addPrefix);
System.out.println(pipeline.apply(" vasya ")); // USER: VASYA
5. Erros comuns na composição de funções
Erro nº 1: Ordem invertida compose/andThen.
Iniciantes frequentemente se confundem sobre o que é executado primeiro e o que vem depois. Lembre-se: f.compose(g) — primeiro g, depois f; f.andThen(g) — primeiro f, depois g.
Erro nº 2: Incompatibilidade de tipos.
Se o tipo do resultado de uma função não coincide com o tipo do parâmetro da outra — o compilador não permitirá combinar. Por exemplo, não é possível fazer Function<Integer, String>.andThen(Function<Double, Boolean>).
Erro nº 3: Cadeias complexas demais.
Às vezes dá vontade de enfiar toda a lógica de negócio em uma única cadeia e obter “código espaguete”. Separe em funções pequenas e dê a elas nomes claros.
Erro nº 4: Efeitos colaterais nas funções.
É melhor manter funções e predicados “puros” (sem efeitos colaterais), caso contrário a composição se torna perigosa e imprevisível.
GO TO FULL VERSION