CodeGym /Cursos /JAVA 25 SELF /Composição de funções: compose, andThen

Composição de funções: compose, andThen

JAVA 25 SELF
Nível 49, Lição 1
Disponível

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

f
g
f.compose(g)
f.andThen(g)
T → R
V → T
V → R: f(g(x))
T → V: g(f(x))

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.

1
Tarefa
JAVA 25 SELF, nível 49, lição 1
Bloqueado
Padronização de logins para a Sociedade Secreta 🕵️‍♂️
Padronização de logins para a Sociedade Secreta 🕵️‍♂️
1
Tarefa
JAVA 25 SELF, nível 49, lição 1
Bloqueado
Transformações mágicas de números na Escola dos Feiticeiros 🧙‍♀️
Transformações mágicas de números na Escola dos Feiticeiros 🧙‍♀️
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION