DEV Community

Cover image for Pasar funciones lambda como argumentos de métodos en Java
Jordi Ayala
Jordi Ayala

Posted on • Originally published at asjordi.dev

Pasar funciones lambda como argumentos de métodos en Java

Si ya conoces las funciones anónimas o lambdas en Java, puede que sepas como crear tus propias interfaces funcionales y utilizarlas como parámetros para otros métodos. En caso de que no, puedes consultar el siguiente post donde se explica cómo funcionan en detalle.

Durante este post, vamos a ver cómo podemos recibir lambdas como parámetro de un método en Java, considerando que una lambda no es más que una forma concisa de representar la implementación de una interfaz funcional, siendo esta una interfaz con un único método abstracto (Single Abstract Method, SAM).

De esta forma, si una lambda no es más que la implementación de una interfaz funcional, podemos invertir la relación y definir un método que acepte como argumento una interfaz funcional, permitiendo de esta manera que se pueda pasar una lambda como argumento para posteriormente ser ejecutada de acuerdo al tipo de la interfaz funcional.

Ejemplo Consumer

Para ilustrar este concepto, vamos a utilizar la interfaz funcional Consumer<T>, la cual representa una operación que acepta un único argumento de tipo T y no devuelve ningún resultado (consume una acción). Esta interfaz funcional tiene un único método abstracto llamado accept que es el que contiene la implementación de la lambda.

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Jon", "Arya");
        print(names, name -> System.out.println("Hello " + name));
    }

    public static void print(List<String> list, Consumer<String> consumer) {
        for (String s : list) {
            consumer.accept(s);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Para este ejemplo, se define el método print() que recibe una lista de nombres y un Consumer<String>. Dentro del método, se itera sobre la lista de nombres y se llama al método accept() del Consumer con cada nombre de la lista.

Hello Jon
Hello Arya
Enter fullscreen mode Exit fullscreen mode

Es importante tener en consideración que una lambda no deja de ser un objeto en Java, por lo que puede tener métodos del tipo default o static. Consumer tiene un método andThen() que permite encadenar otro Consumer después de la ejecución del primero.

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Jon", "Arya");
        print(names, name -> System.out.println("Hello " + name));
    }

    public static void print(List<String> list, Consumer<String> consumer) {
        for (String s : list) {
            consumer.andThen(name -> System.out.println("Goodbye " + name)).accept(s);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

En este caso, se encadena otro Consumer que imprime "Goodbye" seguido del nombre del elemento actual, después de ejecutar el primer Consumer.

Hello Jon
Goodbye Jon
Hello Arya
Goodbye Arya
Enter fullscreen mode Exit fullscreen mode

Ejemplo Predicate

La interfaz funcional Predicate<T> representa una función que acepta un argumento de tipo T y devuelve un valor booleano. Esta interfaz funcional tiene un único método abstracto llamado test que es el que contiene la implementación de la lambda. Se suele utilizar para filtrar elementos de una colección.

import java.util.*;
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
        var evenNumbers = filter(list, i -> i % 2 == 0);
    }

    public static List<Integer> filter(List<Integer> list, Predicate<Integer> predicate) {
        List<Integer> result = new ArrayList<>();
        for (Integer i : list) {
            if (predicate.test(i)) {
                result.add(i);
            }
        }
        return result;
    }
}
Enter fullscreen mode Exit fullscreen mode

En este caso, el método filter() recibe una lista de enteros y un Predicate<Integer>. Dentro del método, se itera sobre la lista de enteros y se llama al método test() del Predicate con cada entero de la lista. Si el resultado es true, se añade el entero a la lista de resultados. Se pasa una lambda como argumento que comprueba si un número es par.

[2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

Al igual que otras interfaces funcionales, Predicate tiene métodos default y static. Por ejemplo, el método negate() devuelve un Predicate que representa la negación de la condición original.

import java.util.*;
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
        var evenNumbers = filter(list, i -> i % 2 == 0);
    }

    public static List<Integer> filter(List<Integer> list, Predicate<Integer> predicate) {
        List<Integer> result = new ArrayList<>();
        for (Integer i : list) {
            if (predicate.negate().test(i)) {
                result.add(i);
            }
        }
        return result;
    }
}
Enter fullscreen mode Exit fullscreen mode

En este caso, se añade el método negate() al Predicate para obtener la negación de la condición original, es decir, se añaden a la lista de resultados los números que no son pares.

[1, 3, 5, 7]
Enter fullscreen mode Exit fullscreen mode

De esta manera, se pueden encadenar múltiples Predicate para filtrar elementos de una colección de acuerdo a diferentes condiciones lógicas.

Ejemplo Function

En su forma más simple la interfaz funcional Function<T, R> representa una función genérica, que acepta un argumento de tipo T y devuelve un resultado de tipo R. Esta interfaz funcional tiene un único método abstracto llamado apply que es el que contiene la implementación de la lambda.

import java.util.*;
import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Ana", "Carlos", "Pedro", "Sofia");
        List<Integer> lengths = convert(names, String::length);
    }

    public static <String, Integer> List<Integer> convert(List<String> lista, Function<String, Integer> f) {
        List<Integer> result = new ArrayList<>();
        for (String e : lista) {
            result.add(f.apply(e));
        }
        return result;
    }
}
Enter fullscreen mode Exit fullscreen mode

El método convert() recibe una lista del tipo String y un Function<String, Integer>. Dentro del método, se itera sobre la lista de cadenas y se llama al método apply() de Function que convierte la cadena en un entero que representa la longitud de esta misma. Se invoca el método pasando una referencia a un método estático String::length como lambda, es decir, una referencia al método length() de la clase String (referencia de método).

[3, 6, 5, 5]
Enter fullscreen mode Exit fullscreen mode

Function tiene otros métodos, por ejemplo, el método andThen() que permite encadenar otra función después de la ejecución de la primera, o el método compose() que permite realizar composición de funciones. En cualquiera de los casos, todo depende de la lógica que se quiera implementar al utilizar esta interfaz funcional.

Conclusión

En resumen, lo único que se debe tener en consideración en caso de requerir pasar una lambda como argumento de un método, es que una función anónima o lambda no deja de ser una implementación de una interfaz funcional, por lo que se puede tomar este concepto y aplicarlo a cualquier método que acepte una interfaz funcional como argumento, obteniendo de esta forma un código más limpio y conciso, e incluso aprovechando los métodos default y static que la interfaz pueda tener para realizar operaciones más complejas.

Top comments (0)