CodeGym /Cursos /JAVA 25 SELF /Pasar funciones como parámetros: ejemplos

Pasar funciones como parámetros: ejemplos

JAVA 25 SELF
Nivel 49 , Lección 2
Disponible

1. Pasar comportamiento en lugar de datos

¿Por qué pasar una función como parámetro?

Imagina que tienes un método que ordena una lista. Pero, ¿cómo sabe exactamente cómo ordenar? ¿Alfabéticamente? ¿Por longitud? ¿Por fecha de nacimiento? Podrías escribir un método aparte para cada caso, pero eso se convertiría rápidamente en un infierno de copia y pega.

En su lugar, Java permite pasar comportamiento (es decir, una función o una lambda) que determina cómo ordenar, filtrar, transformar, etc. Esto hace que el código sea flexible y reutilizable.

Ejemplo: ordenación con un comparator

List<String> names = List.of("Anna", "Boris", "Vika");

// Pasamos el comportamiento: cómo comparar los elementos (por longitud de la cadena)
names.stream()
     .sorted((a, b) -> a.length() - b.length())
     .forEach(System.out::println);

Ejemplo: filtrado con un predicate

List<String> names = List.of("Anna", "Boris", "Vika");

// Pasamos el comportamiento: a quién mantener (nombre de más de 4 caracteres)
names.stream()
     .filter(name -> name.length() > 4)
     .forEach(System.out::println);

En ambos casos no «fijaste» rígidamente la lógica en el método, sino que le diste un trozo de comportamiento que se aplica a cada elemento.

Ventajas: menos duplicación, más flexibilidad

En lugar de decenas de métodos con código similar pero lógica distinta, escribes un único método genérico que acepta «qué hacer» en forma de función. Esto ahorra tiempo, reduce errores y hace el código más fácil de probar.

2. Sintaxis para pasar funciones

Expresiones lambda como argumentos

La forma más habitual es escribir directamente en el lugar de la llamada una lambda con la flecha ->:

list.forEach(item -> System.out.println(item));

O incluso más corto, si ya existe un método adecuado: una referencia a método (::):

list.forEach(System.out::println);

Referencias a métodos (method references)

Si ya tienes un método adecuado, puedes pasarlo como referencia :::

// Método normal
public static boolean isLongName(String name) {
    return name.length() > 4;
}

// Paso de una referencia a método
names.stream()
     .filter(MyClass::isLongName)
     .forEach(System.out::println);

Esto funciona si la firma del método coincide con la esperada por la interfaz funcional (Predicate<T>, Function<T, R>, Comparator<T>, etc.).

3. Ejemplos de la biblioteca estándar

Collections.sort y Comparator

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));

// Orden por longitud del nombre
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [Vika, Anna, Boris]

Stream.filter y Predicate

List<String> names = List.of("Anna", "Boris", "Vika");

// Dejar solo los nombres que empiezan por 'V'
names.stream()
     .filter(name -> name.startsWith("V"))
     .forEach(System.out::println); // Vika

Stream.map y Function

List<String> names = List.of("Anna", "Boris", "Vika");

// Convertir los nombres a mayúsculas
names.stream()
     .map(String::toUpperCase)
     .forEach(System.out::println);

Optional.ifPresent y Consumer

Optional<String> opt = Optional.of("¡Hola!");

// Si hay valor, imprimirlo
opt.ifPresent(s -> System.out.println("En la cadena: " + s));

4. Práctica: escribimos nuestros propios métodos con funciones-parámetro

¡Hora de poner en práctica lo aprendido! Escribamos varios métodos que aceptan una función como parámetro y la usan internamente.

Ejemplo 1: Un método para procesar elementos de una lista (forEach a tu manera)

Supongamos que tenemos una lista de usuarios (User). Queremos realizar alguna acción sobre cada usuario: por ejemplo, mostrar el nombre, enviar un e-mail, abonar bonos, etc. En lugar de «fijar» rígidamente la acción, la pasaremos como parámetro — ¡un 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 acepta un 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)
        );

        // Imprimir los nombres de todos los usuarios
        processUsers(users, user -> System.out.println("Nombre: " + user.name));

        // Abonar un bono (para el ejemplo, solo mostrarlo)
        processUsers(users, user -> System.out.println(user.name + " recibió un bono!"));
    }
}

Ejemplo 2: Método para filtrar elementos (Predicate)

Escribamos un método que devuelva solo los usuarios que cumplan una condición (por ejemplo, solo los mayores de edad):

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)
        );

        // Filtramos solo a los mayores de edad
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        adults.forEach(user -> System.out.println(user.name)); // Anna, Vika
    }
}

Ejemplo 3: Método transformador (Function)

Ahora, un método que a partir de una lista de usuarios hace una lista de sus nombres (u otras transformaciones):

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)
        );

        // Obtenemos la lista de nombres
        List<String> names = mapUsers(users, user -> user.name);
        System.out.println(names); // [Anna, Boris, Vika]

        // Obtenemos la lista de edades
        List<Integer> ages = mapUsers(users, user -> user.age);
        System.out.println(ages); // [25, 17, 22]
    }
}

Ejemplo 4: Método generador (Supplier)

A veces necesitamos obtener un valor «bajo demanda»: por ejemplo, generar un número aleatorio, crear un objeto, obtener la hora actual. Para ello sirve la interfaz 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) {
        // Obtenemos un número aleatorio
        int random = getValue(() -> (int)(Math.random() * 100));
        System.out.println("Número aleatorio: " + random);

        // Obtenemos la hora actual en milisegundos
        long time = getValue(System::currentTimeMillis);
        System.out.println("Hora: " + time);
    }
}

5. Aplicación unificada: conectamos los ejemplos

Imaginemos que desarrollamos una aplicación sencilla «Lista de usuarios». Ya sabemos:

  • Filtrar usuarios por condición (por ejemplo, solo mayores de edad);
  • Transformar usuarios en algo (nombres, e-mail, edad);
  • Ejecutar acciones sobre cada usuario (impresión, abono de bonos);
  • Generar valores bajo demanda (por ejemplo, crear nuevos usuarios).

Ahora, usando todos estos métodos, podemos construir flujos flexibles de procesamiento de datos, sin reescribir el código cada vez para una tarea nueva.

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 procesar usuarios
    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. Imprimir todos los usuarios
        processUsers(users, user -> System.out.println("Usuario: " + user));

        // 2. Encontrar solo a los adultos
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        System.out.println("Mayores de edad: " + adults);

        // 3. Obtener los nombres de los adultos
        List<String> adultNames = mapUsers(adults, user -> user.name);
        System.out.println("Nombres de los adultos: " + adultNames);

        // 4. Abonar un bono a los adultos
        processUsers(adults, user -> System.out.println(user.name + " recibió un bono!"));
    }
}

¿Por qué es mejor que el enfoque «normal»?

Este enfoque da flexibilidad: los mismos métodos se pueden usar en escenarios muy distintos simplemente pasando funciones diferentes. El código no crece en infinitas variantes de «filtramos así», «imprimimos asá»; tenemos herramientas generales que funcionan en todas partes. Gracias a esto, resulta más compacto, más claro y menos propenso a errores. Además, este estilo encaja perfectamente con el moderno Stream API, así que al pasar a streams no tendrás que volver a aprender: el enfoque es el mismo.

6. Errores típicos al pasar funciones como parámetros

Error n.º 1: la firma no coincide con la interfaz esperada.
Si un método acepta Predicate<User> y tú pasas una lambda que devuelve no boolean, sino, por ejemplo, String, el compilador enseguida dirá «¡ay, ay, ay!» y no dejará compilar el proyecto. Comprueba que el valor devuelto y los parámetros coinciden con lo esperado.

Error n.º 2: la lambda usa variables que pueden cambiar.
En Java, las expresiones lambda solo pueden usar variables final o effectively final del contexto externo. Si intentas modificar una de esas variables dentro de la lambda, obtendrás un error de compilación.

Error n.º 3: confusión entre interfaces.
A veces quieres pasar, por ejemplo, un Consumer<T>, pero por accidente escribes una función que devuelve algo (por ejemplo, Function<T, R>). Comprueba que tu lambda devuelve exactamente lo que se necesita (o nada, en el caso de Consumer).

Error n.º 4: lambdas demasiado complejas directamente en los parámetros.
Si una lambda ocupa más de una o dos líneas, mejor extraerla a una variable o método aparte. De lo contrario, el código se volverá ilegible y solo tú te aclararás con él (y no siempre).

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