1. Concepto de composición de funciones
Un poco de teoría (sin ser pesada)
En matemáticas, la composición de funciones es cuando el resultado de una función se convierte en la entrada de otra. Si tenemos las funciones f y g, la composición g(f(x)) significa: primero aplicamos f a x y luego pasamos el resultado a g.
En programación la idea es la misma: queremos construir transformaciones complejas a partir de sencillas, para no escribir una función gigantesca que lo haga todo. Esto hace el código flexible, reutilizable y legible.
Imagina una cadena de montaje de pasteles: primero la masa (f), luego la crema (g), después la decoración (h). Todo el proceso es h(g(f(ingredientes))).
¿Por qué es importante la composición?
- La composición permite construir el programa a partir de pequeñas funciones «bloques».
- Más fácil de reutilizar: un «bloque» puede insertarse en distintos lugares sin duplicación.
- Flexibilidad: para cambiar una etapa, sustituimos la función correspondiente y no tocamos el resto.
- Legibilidad y capacidad de prueba: las funciones pequeñas son más fáciles de leer, probar y mantener.
2. Métodos compose y andThen en la interfaz Function
Interfaz Function: recordatorio
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// Métodos de composición:
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: primero se ejecuta la función que pasas a compose y después la función actual.
- andThen: primero se ejecuta la función actual y después la que se pasa a andThen.
Esquema visual
// Supongamos que hay dos funciones:
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
// ¡Pero el orden importa si los tipos son distintos!
Ejemplo: convertir una cadena en un número y luego elevar al cuadrado
import java.util.function.Function;
public class ComposeAndThenDemo {
public static void main(String[] args) {
// Función: convierte una cadena en número
Function<String, Integer> parse = s -> Integer.parseInt(s);
// Función: eleva un número al cuadrado
Function<Integer, Integer> square = x -> x * x;
// Combinamos: primero parseamos, luego elevamos al cuadrado
Function<String, Integer> parseThenSquare = parse.andThen(square);
System.out.println(parseThenSquare.apply("7")); // 49
// ¿Y si cambiamos el orden?
// square.compose(parse) — el mismo resultado (para estas funciones)
Function<String, Integer> squareOfParsed = square.compose(parse);
System.out.println(squareOfParsed.apply("8")); // 64
}
}
¿Cuándo importa el orden?
Si los tipos de las funciones no coinciden, el orden se vuelve crítico. Por ejemplo:
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
// Así:
// length.andThen(addPrefix) — ¡error de compilación!
// length devuelve Integer, y addPrefix acepta String.
Tabla: diferencia entre compose y andThen
|
|
|
|
|---|---|---|---|
|
|
|
|
3. Composición de predicados y otras interfaces
Predicate<T>: and, or, negate
La interfaz funcional Predicate<T> es una función que devuelve boolean. Para combinar predicados hay métodos específicos:
- and: AND lógico (&&)
- or: OR lógico (||)
- negate: NOT lógico (!)
Ejemplo: condiciones de filtrado complejas
Supongamos que tenemos una clase de usuario:
public class User {
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
Ahora escribamos varios predicados:
import java.util.function.Predicate;
Predicate<User> isAdult = user -> user.age >= 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");
// Combinamos: adulto y nombre que empieza por "A"
Predicate<User> adultAndA = isAdult.and(nameStartsWithA);
// Adulto o nombre que empieza por "A"
Predicate<User> adultOrA = isAdult.or(nameStartsWithA);
// No adulto
Predicate<User> notAdult = isAdult.negate();
Ahora podemos usar estos predicados en el filtrado, por ejemplo, con la API de 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());
// En filtered solo entrará Alice (adulta y nombre que empieza por "A")
Composición de Consumer, Function, Supplier
- Consumer<T>: el método andThen permite ejecutar dos operaciones seguidas.
- Function<T, R>: la composición ya la hemos visto arriba.
- Supplier<T>: no se combina directamente, pero puede usarse dentro de otras funciones.
Ejemplo: Consumer<T>.andThen
import java.util.function.Consumer;
Consumer<String> print = s -> System.out.println("Recibido: " + s);
Consumer<String> printUpper = s -> System.out.println("En mayúsculas: " + s.toUpperCase());
Consumer<String> combined = print.andThen(printUpper);
combined.accept("hello");
// Imprimirá:
// Recibido: hello
// En mayúsculas: HELLO
4. Práctica: cadenas de transformaciones y filtrados
Tarea 1: componer una cadena de transformaciones Function
Supongamos que en nuestra aplicación almacenamos usuarios como cadenas "Nombre,Edad", por ejemplo "Alice,20". Hay que:
- Transformar la cadena en un objeto User
- Obtener la edad
- Comprobar si el usuario es mayor de edad
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;
// Combinamos: cadena -> User -> edad -> predicado
Function<String, Integer> stringToAge = stringToUser.andThen(getAge);
String input = "Bob,19";
int age = stringToAge.apply(input);
System.out.println("Edad: " + age); // 19
System.out.println("¿Mayor de edad? " + isAdultAge.test(age)); // true
Tarea 2: combinar varios Predicate para filtrar
Supongamos que necesitamos escoger usuarios que sean mayores de 18 y cuyo nombre empiece por «A» o «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());
Tarea 3: transformación de varios pasos con Function
Supongamos que necesitamos: recibir una cadena, recortar espacios, pasar a mayúsculas y añadir el prefijo "USER: ".
Function<String, String> trim = String::trim;
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> addPrefix = s -> "USER: " + s;
// Construimos la cadena
Function<String, String> pipeline = trim.andThen(toUpper).andThen(addPrefix);
System.out.println(pipeline.apply(" user ")); // USER: VASYA
5. Errores típicos al componer funciones
Error n.º 1: orden confundido entre compose/andThen.
Los principiantes a menudo confunden qué se ejecuta primero y qué después. Recuerda: f.compose(g) — primero g, después f; f.andThen(g) — primero f, después g.
Error n.º 2: incompatibilidad de tipos.
Si el tipo de resultado de una función no coincide con el tipo del parámetro de otra, el compilador no permitirá combinarlas. Por ejemplo, no se puede hacer Function<Integer, String>.andThen(Function<Double, Boolean>).
Error n.º 3: cadenas demasiado complejas.
A veces apetece meter toda la lógica de negocio en una sola cadena y acabamos con código espagueti. Divide en funciones pequeñas y dales nombres claros.
Error n.º 4: efectos secundarios en las funciones.
Es mejor mantener las funciones y predicados «puros» (sin efectos secundarios), de lo contrario la composición se vuelve peligrosa e impredecible.
GO TO FULL VERSION