Saltar a contenido

lambda

Entidades de java.util.functional

Una interfaz funcional es una interfaz de Java que tiene un solo método abstracto. Se utilizan principalmente para trabajar con expresiones lambda y programación funcional.

Vienen anotadas con @FunctionalInterface. No es obligatoria, pero se recomienda.

@FunctionalInterface
interface Saludo {
    void decirHola();
}
Si intentas añadir otro método abstracto, el compilador dará error.

La Api de Java incluye en el paquete java.util.function una serie de interfaces funcionales que se utilizan en otras clases/interfaces de la Api o que podemos usar en nuestro propias clases.

Las implementaciones de estas interfaces son del tipo, consume un valor y retorna otro tipo de valor, o produce un valor sin argumentos o produce un valor dados dos argumentos.

A éstas se les llama unidades funcionales porque componen una lógica interna que a priori el consumidor de esta lógica no conoce, pero de la que sí se conoce su interfaz y por tanto la manera de relacionarse con el resto de los objetos, o lo que es lo mismo la manera de ser invocada

Las interfaces funcionales más importantes contenidas en java.util.function son:

lambda

  • Predicate: Se utiliza en expresiones lambda para comprobar si una condición dada es verdadera o falsa
  • Supplier: esta función se debe utilizar cuando se necesiten generar objetos sin requerir argumentos. Por ejemplo para realizar una inicialización perezosa.
  • Consumer esta en cambio es el opuesto de Supplier ya que consume, acepta como argumento el tipo T sin devolver ningún valor de retorno.
  • Function<T,R> esta interfaz permite definir una función que acepta un parámetro de tipo T y devuelve un resultado del tipo R pudiendo aplicarle alguna transformación u operación.
  • BiFunction<T,R,S> esta interfaz permite definir una función que acepta dos parámetros de tipo T y R, devolviendo un resultado del tipo S. Normalmente serán operaciones de agregación u operadores binarios como la suma, resta, etc..

Predicate

Predicate la interfaz predicado debe devolver forzosamente un boolean dado un objeto de tipo T, normalmente utilizado para filtrar elementos de una colección.

Recibe un valor y devuelve true o false

Esta es la estructura de la interfaz Predicate:

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
}

Como vemos la interfaz Predicate utiliza los genéricos para poder decirle que tipo concreto vamos a utilizar.

Ejemplo:

public class EjemploPredicate {
    public static void main(String[] args) {

        Predicate<Integer> esMayorQue10 = n -> n > 10;
        System.out.println(esMayorQue10.test(15)); // true

        Predicate<String> checker = a -> a.startsWith("M");
        System.out.println(checker.test("Miguel"));
    }
}

Nos permitirá recibir en un método como parámetro una condición independiente del algoritmo.

El siguiente ejemplo el método recibe una lista y un objeto de tipo predicate que será la condición. Imprime aquellos elementos que cumplen la condición

public static void procesar(List<Integer> lista, Predicate<Integer> condicion) {
        for (Integer n : lista) {
            //será True/False en función del predicate
            if (condicion.test(n)) {
                System.out.println(n);
            }
        }
    }
La llamada sería
public static void main(String[] args) {
        List<Integer> numeros = List.of(10, 22, 8, 15, 4);

        // Predicate: números pares
        Predicate<Integer> esPar = n -> n % 2 == 0;

        // Llamada al método pasando el predicate
        procesar(numeros, esPar);

        // Predicate: menores que 10
        Predicate<Integer> esMenor10 = n -> n < 10;

        // Llamada al método pasando el predicate
        procesar(numeros, esMenor10);

    }

Métodos Predicate

La interfaz Predicate contiene algunos métodos como:

  • isEqual(Object targetRef): Devuelve un predicado que prueba si dos argumentos son iguales.
  • and(Predicate other): Devuelve un predicado compuesto que representa un AND lógico de este predicado y otro.
  • or(Predicate other): Devuelve un predicado compuesto que representa un OR lógico de este predicado y otro.
  • negate(): Devuelve un predicado que representa la negación lógica de este predicado.

Ejemplo:

    Predicate<Integer> greaterThan10 = i -> i > 10;
    Predicate<Integer> lessThan20 = i -> i < 20;
    //si es mayor de 10 y menor de 20->true
    Predicate<Integer> andPredicate=greaterThan10.and(lessThan20);

    System.out.println(andPredicate.test(15));//true
    //negate, niega el resultado del predicado
    System.out.println(andPredicate.negate().test(15));//false

    //isEquals, Devuelve un predicado que prueba si dos argumentos son iguales 
    //según Objects.equals(Object, Object).
    Predicate<String> compara  = Predicate.isEqual("hola");
    System.out.println(compara.test("java"));//false
    System.out.println(compara.test("hola"));//true
Siguiendo el ejemplo de la lista

// menores de 10 y pares
procesar(numeros, esMenor10.and(esPar));
//mayores o iguales de 10
procesar(numeros, esMenor10.negate())
//impares menores de 10
procesar(numeros,esPar.negate().and(esMenor10))

Nota

Debido al uso extendido de Predicate se han añadido las interfaces funcionales IntPredicate cuando queremos trabajar con predicados de tipo entero, DoublePredicate y LongPredicate. También tenemos la interfaz BiPredicate que es un caso especial de Predicate y recibe dos parámetros en vez de uno.

    IntPredicate predicate = (x) -> {
        if (x == 12345) {
            return true;
        }
        return false;
    };

    System.out.println(predicate.test(12345));
    //creamos otro predicado que niega el anterior
    IntPredicate intPredicate1 = predicate.negate();
    System.out.println(intPredicate1.test(12345));

    BiPredicate<String, Integer> filtroLongitud = (x, y) -> {
        return x.length() == y;
    };

    boolean result = filtroLongitud.test("java", 10);
    System.out.println(result); // false

Supplier

Supplier es otra interfaz funcional dentro del paquete java.util.function que nos provee del método abstracto **get**, sin argumentos que devuelve un tipo de dato.

@FunctionalInterface
public interface Supplier<T> {

    T get();
}

Esta interfaz también se utiliza con expresiones lambda que no tienen parámetros pero devuelven un resultado:

    Random random = new Random();
    //devuelve un número aleatorio entre 1..6
    Supplier<Integer> dados = () -> random.nextInt(6)+1;
    System.out.println("Jugada 1: " +dados.get());
    System.out.println("Jugada 2: " +dados.get());

Al igual que ocurría en los predicados con los Supplier también disponemos de las clases IntSupplier, DoubleSupplier, LongSupplier y BooleanSupplier.

Consumer

Consumer es otra interfaz funcional dentro del paquete java.util.function que provee un método que recibe un solo parámetro de tipo genérico y no devuelve nada.

public interface Consumer<T> {
    void accept(T t);
}

La expresión lambda asignada a un objeto de tipo Consumer se usa para definir su método **accept(T t)** que eventualmente aplica la operación dada en su argumento. Los Consumer son útiles cuando no necesitan devolver ningún valor, ya que se espera que operen a través de efectos secundarios.

Existen también las interfaces IntConsumer, LongConsumer y DoubleConsumer.

Consumer<Integer> doble = (x) -> System.out.println(x*2);
doble.accept(5);
El método foreach de Collection que hemos visto anteriormente, acepta un Consumer

default void forEach(Consumer<? super T> action)
public static void main(String[] args) {  

    List<String> list = new ArrayList<String>();  
    list.add("java");
    list.add("lambda");
    list.add("test");  

    list.forEach(  
        //n es el elemento actual de la lista
        (n)->System.out.println(n)  
    );  

    List<Integer> list2=new ArrayList<Integer>();
    list2.add(1);
    list2.add((2));
    list2.add(3);

    list2.forEach(
        n->System.out.println(
            n%2==0?"Es par":"Es impar"
            )
        );
}

BiConsumer es un caso especial de las expresiones Consumer, son aquellas que reciben dos valores como parámetro y no devuelven resultado.

@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);
}

Ejemplo:

BiConsumer<Integer, String> biConsumer = (x, s) -> System.out.println(x + s);
biConsumer.accept(3, " puntos");

Biconsumer y Map

Map tiene un método abstracto que foreach que recibe un Biconsumer, donde el primer valor es la clave y el segundo el valor

void forEach(BiConsumer<? super K,? super V> action)
Podemos simplificar el recorrido de un mapa

Map<String, Integer> edades = new HashMap<>();
edades.put("Ana", 25);
edades.put("Luis", 30);
edades.put("Marta", 28);

BiConsumer<String, Integer> imprimir =
        (nombre, edad) -> System.out.println(nombre + ": " + edad);

edades.forEach(imprimir);
o más simple

edades.forEach(
    (nombre, edad) ->
        System.out.println(nombre + ": " + edad)
        );

Function

Nos permite recibir un parámetro, ejecutar un código y devolver un valor mediante el método apply.

Su principal uso es la transformación de un valor en otro

Function<String, Integer> funcionLambda = (s) -> {
        int total = 0;
        for (int i = 0; i < s.length(); i++) {
            //suma el valor del código Unicode del caracter
            total+=s.charAt(i);
        }
        return total;
    };
System.out.println(funcionLambda.apply("Programación"));

//devuelve el cuadrado
Function<Integer, Integer> cuadrado =
                n -> n * n;

int resultado = cuadrado.apply(4);

System.out.println("Cuadrado: " + resultado);
Vamos a ver un ejemplo más complejo en el que una función transforma cada elemento de una lista según la Funcition pasada como parámetro

// Método que transforma una lista usando Function
public static void transformarLista(List<Integer> lista, Function<Integer, Integer> funcion) {

        for (int i = 0; i < lista.size(); i++) {
            int valor = lista.get(i);
            int nuevoValor = funcion.apply(valor);
            lista.set(i, nuevoValor);
        }
    }
podemos llamarla enviado distintas transformaciones

public static void main(String[] args) {

    List<Integer> numeros = List.of(1, 2, 3, 4);

    // 1ª llamada: multiplicar por 2
    List<Integer> copia1 = new ArrayList<>(numeros);
    transformarLista(copia1, n -> n * 2);
    System.out.println(copia1);

    // 2ª llamada: sumar 10
    List<Integer> copia2 = new ArrayList<>(numeros);
    transformarLista(copia2, n -> n + 10);
    System.out.println(copia2);

    // 3ª llamada: cuadrado
    List<Integer> copia3 = new ArrayList<>(numeros);
    transformarLista(copia3, n -> n * n);
    System.out.println(copia3);
}
[2, 4, 6, 8]
[11, 12, 13, 14]
[1, 4, 9, 16]

Al igual que las otras interfaces, tenemos variantes que nos simplifican el código

Interfaz Firma Tipo de transformación Ejemplo de lambda
Function<T,R> T → R Objeto → objeto s -> s.length()
BiFunction<T,U,R> (T,U) → R Dos objetos → objeto (a,b) -> a + b
UnaryOperator<T> T → T Objeto → mismo objeto n -> n * n
BinaryOperator<T> (T,T) → T Dos objetos → mismo objeto (a,b) -> a + b

Especializadas para transformar Objeto a primitivo

Interfaz Firma Transformación Ejemplo
ToIntFunction<T> T → int Objeto → int s -> s.length()
ToDoubleFunction<T> T → double Objeto → double p -> p.getPrecio()
ToLongFunction<T> T → long Objeto → long d -> d.getTime()

Especializadas para transformar primitivo a primitivo

Interfaz Firma Transformación Ejemplo
IntToDoubleFunction int → double int → double n -> n * 1.5
IntToLongFunction int → long int → long n -> n * 1000L
DoubleToIntFunction double → int double → int d -> (int) d
DoubleToLongFunction double → long double → long d -> (long) d
LongToIntFunction long → int long → int l -> (int) l
LongToDoubleFunction long → double long → double l -> l / 2.0