CodeGym /Cursos /JAVA 25 SELF /Interfaces funcionales: Predicate, Consumer, Supplier, Fu...

Interfaces funcionales: Predicate, Consumer, Supplier, Function

JAVA 25 SELF
Nivel 49 , Lección 0
Disponible

1. Profundizamos en la interfaz funcional

Una interfaz funcional es aquella que tiene EXACTAMENTE un método abstracto (es decir, no implementado). Precisamente por eso Java entiende: «ajá, ¡aquí se puede pasar una lambda!» o una referencia a método.

Para evitar errores, estas interfaces suelen llevar la anotación @FunctionalInterface. No es obligatoria, pero si la añades y por accidente escribes un segundo método abstracto, el compilador se quejará de inmediato.

Ejemplo:

@FunctionalInterface
interface MyAction {
    void run();
}
MyAction action = () -> System.out.println("¡Hola desde la lambda!");
action.run(); // Imprimirá: ¡Hola desde la lambda!

¿Para qué sirve?

  • Permite usar expresiones lambda y referencias a métodos en lugar de crear clases anónimas (¡menos boilerplate!).
  • Le indica al compilador que la interfaz está pensada para programación funcional.

Dato interesante: En la biblioteca estándar de Java ya hay decenas de estas interfaces — ¡no hace falta reinventar la rueda!

2. Visión general de las interfaces funcionales estándar

En el paquete java.util.function viven decenas de interfaces funcionales. Consideremos las cuatro más populares (tienen la mayor «afluencia» entre todas las interfaces de Java).

Interfaz Qué recibe Qué devuelve Para qué se usa normalmente
Predicate<T>
T
boolean
Comprobación de condición (filtrado)
Consumer<T>
T
void
Realizar una acción sobre el objeto
Supplier<T>
nada
T
Obtener/generar un objeto
Function<T, R>
T
R
Convertir T en R

Predicate<T>

Descripción: Función que recibe un objeto de tipo T y devuelve true o false. Ejemplo típico: filtrado de una lista. Método clave — test.

Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Java"));       // false
System.out.println(isLong.test("Functional")); // true

Consumer<T>

Descripción: Recibe un objeto de tipo T y realiza una acción con él, no devuelve nada. Método clave — accept.

Consumer<String> printer = s -> System.out.println("Imprimo: " + s);
printer.accept("Hello, world!"); // Imprimo: Hello, world!

Supplier<T>

Descripción: No recibe nada y devuelve un objeto de tipo T. Se puede imaginar como un «generador» de valores. Método clave — get.

Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // Por ejemplo, 0.1234567

Function<T, R>

Descripción: Recibe un objeto de tipo T y devuelve un objeto de tipo R. Ejemplo típico: transformación de datos. Método clave — apply.

Function<String, Integer> stringToLength = s -> s.length();
System.out.println(stringToLength.apply("Java")); // 4

En breve: UnaryOperator, BinaryOperator, BiFunction

  • UnaryOperator<T> — lo mismo que Function<T, T>: recibe y devuelve el mismo tipo.
  • BinaryOperator<T> — lo mismo que BiFunction<T, T, T>: recibe dos T y devuelve un T.
  • BiFunction<T, U, R> — recibe dos tipos distintos y devuelve un tercero.
UnaryOperator<Integer> square = x -> x * x;
BinaryOperator<Integer> sum = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);

3. Ejemplos de uso

Veamos cómo aparecen estas interfaces en tareas reales y, en especial, en colecciones y el Stream API.

Paso a métodos de colecciones y Stream API

Ejemplo 1: Predicate y filtrado

List<String> words = List.of("java", "stream", "lambda", "code");
List<String> longWords = words.stream()
    .filter(word -> word.length() > 4) // Predicate<String>
    .toList();
System.out.println(longWords); // [stream, lambda]

Ejemplo 2: Consumer y forEach

words.forEach(word -> System.out.println("Palabra: " + word)); // Consumer<String>

Ejemplo 3: Function y map

List<Integer> lengths = words.stream()
    .map(word -> word.length()) // Function<String, Integer>
    .toList();
System.out.println(lengths); // [4, 6, 6, 4]

Ejemplo 4: Supplier y generación de valores

Supplier<String> greetingSupplier = () -> "¡Hola, Java!";
System.out.println(greetingSupplier.get()); // ¡Hola, Java!

Comparación con clases anónimas

Antes había que escribir así:

Predicate<String> isShort = new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.length() < 5;
    }
};

Con lambdas es mucho más agradable:

Predicate<String> isShort = s -> s.length() < 5;

4. Práctica: escribimos expresiones lambda para cada interfaz

Implementemos una pequeña aplicación — una lista de usuarios. Cada usuario estará representado por la clase User:

public class User {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

Creemos una lista de usuarios:

List<User> users = List.of(
    new User("Anna", 23),
    new User("Boris", 17),
    new User("Vika", 31),
    new User("Gosha", 15)
);

Predicate: filtrado de adultos

Predicate<User> isAdult = user -> user.getAge() >= 18;
List<User> adults = users.stream()
    .filter(isAdult)
    .toList();

System.out.println("Adultos: " + adults); // Adultos: [Anna (23), Vika (31)]

Consumer: impresión de usuarios

Consumer<User> printUser = user -> System.out.println("Usuario: " + user);
adults.forEach(printUser);

Supplier: generación de usuarios

Supplier<User> randomUserSupplier = () -> {
    String[] names = {"Dima", "Katya", "Lyosha"};
    int randomAge = 10 + (int)(Math.random() * 30);
    String randomName = names[(int)(Math.random() * names.length)];
    return new User(randomName, randomAge);
};

User randomUser = randomUserSupplier.get();
System.out.println("Usuario aleatorio: " + randomUser);

Function: obtener el nombre del usuario

Function<User, String> getName = user -> user.getName();
List<String> names = users.stream()
    .map(getName)
    .toList();

System.out.println("Nombres: " + names); // Nombres: [Anna, Boris, Vika, Gosha]

5. Matices útiles

Uso en el Stream API: filter, map, forEach y otros

Unamos todo y escribamos una cadena de transformaciones:

users.stream()
     .filter(user -> user.getAge() >= 18)         // Predicate<User>
     .map(user -> user.getName().toUpperCase())    // Function<User, String>
     .forEach(name -> System.out.println("Adulto: " + name)); // Consumer<String>

Resultado:

Adulto: ANNA
Adulto: VIKA

Tabla de referencia: qué usar en cada caso

Dónde se usa Qué interfaz se necesita Ejemplo de uso
filter (Stream)
Predicate<T>
filter(u -> u.getAge() > 18)
map (Stream)
Function<T, R>
map(u -> u.getName())
forEach (Stream, List)
Consumer<T>
forEach(u -> System.out.println(u))
generate (Stream)
Supplier<T>
Stream.generate(() -> ...)

¿Por qué es importante conocer las interfaces funcionales?

  • Son la base de todas las expresiones lambda en Java.
  • Permiten escribir código universal, reutilizable y conciso.
  • Simplifican el trabajo con colecciones, flujos y tareas asíncronas.

¿Cuándo usar cada interfaz?

  • Predicate — cuando necesitas comprobar una condición (filtrar, buscar).
  • Consumer — cuando necesitas hacer algo con el objeto (imprimir, guardar, enviar).
  • Supplier — cuando necesitas obtener o generar un objeto (fábricas, generadores).
  • Function — cuando necesitas transformar un objeto de un tipo a otro.

6. Errores típicos al trabajar con interfaces funcionales

Error n.º 1: elección incorrecta de la interfaz. A veces los principiantes confunden Predicate y Function — por ejemplo, intentan devolver boolean desde Function en lugar de Predicate. Recuerda: Predicate siempre devuelve boolean; Function, cualquier otro tipo.

Error n.º 2: no utilizar las interfaces estándar. A menudo se escriben interfaces propias como «Checker» con el método boolean check(T t) en vez de usar Predicate. Es mejor usar las estándar: están soportadas en todas partes y hacen el código más claro para otros desarrolladores.

Error n.º 3: lambda demasiado compleja. Si una lambda se convierte en una mini-novela de 10 líneas, conviene extraerla a un método o clase aparte. La lambda es por brevedad y legibilidad.

Error n.º 4: olvidar la anotación @FunctionalInterface. Si escribes tu propia interfaz funcional, no olvides la anotación. Te protegerá de errores accidentales (como añadir un segundo método abstracto).

Error n.º 5: uso de estado mutable dentro de la lambda. Si la lambda modifica variables externas o colecciones, puede provocar errores inesperados, especialmente al trabajar con hilos. Es mejor evitar efectos secundarios.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION